github.com/artpar/rclone@v1.67.3/backend/googlephotos/pattern.go (about) 1 // Store the parsing of file patterns 2 3 package googlephotos 4 5 import ( 6 "context" 7 "fmt" 8 "path" 9 "regexp" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/artpar/rclone/backend/googlephotos/api" 15 "github.com/artpar/rclone/fs" 16 ) 17 18 // lister describes the subset of the interfaces on Fs needed for the 19 // file pattern parsing 20 type lister interface { 21 listDir(ctx context.Context, prefix string, filter api.SearchFilter) (entries fs.DirEntries, err error) 22 listAlbums(ctx context.Context, shared bool) (all *albums, err error) 23 listUploads(ctx context.Context, dir string) (entries fs.DirEntries, err error) 24 dirTime() time.Time 25 startYear() int 26 includeArchived() bool 27 } 28 29 // dirPattern describes a single directory pattern 30 type dirPattern struct { 31 re string // match for the path 32 match *regexp.Regexp // compiled match 33 canUpload bool // true if can upload here 34 canMkdir bool // true if can make a directory here 35 isFile bool // true if this is a file 36 isUpload bool // true if this is the upload directory 37 // function to turn a match into DirEntries 38 toEntries func(ctx context.Context, f lister, prefix string, match []string) (fs.DirEntries, error) 39 } 40 41 // dirPatterns is a slice of all the directory patterns 42 type dirPatterns []dirPattern 43 44 // patterns describes the layout of the google photos backend file system. 45 // 46 // NB no trailing / on paths 47 var patterns = dirPatterns{ 48 { 49 re: `^$`, 50 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (fs.DirEntries, error) { 51 return fs.DirEntries{ 52 fs.NewDir(prefix+"media", f.dirTime()), 53 fs.NewDir(prefix+"album", f.dirTime()), 54 fs.NewDir(prefix+"shared-album", f.dirTime()), 55 fs.NewDir(prefix+"upload", f.dirTime()), 56 fs.NewDir(prefix+"feature", f.dirTime()), 57 }, nil 58 }, 59 }, 60 { 61 re: `^upload(?:/(.*))?$`, 62 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (fs.DirEntries, error) { 63 return f.listUploads(ctx, match[0]) 64 }, 65 canUpload: true, 66 canMkdir: true, 67 isUpload: true, 68 }, 69 { 70 re: `^upload/(.*)$`, 71 isFile: true, 72 canUpload: true, 73 isUpload: true, 74 }, 75 { 76 re: `^media$`, 77 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (fs.DirEntries, error) { 78 return fs.DirEntries{ 79 fs.NewDir(prefix+"all", f.dirTime()), 80 fs.NewDir(prefix+"by-year", f.dirTime()), 81 fs.NewDir(prefix+"by-month", f.dirTime()), 82 fs.NewDir(prefix+"by-day", f.dirTime()), 83 }, nil 84 }, 85 }, 86 { 87 re: `^media/all$`, 88 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (fs.DirEntries, error) { 89 return f.listDir(ctx, prefix, api.SearchFilter{}) 90 }, 91 }, 92 { 93 re: `^media/all/([^/]+)$`, 94 isFile: true, 95 }, 96 { 97 re: `^media/by-year$`, 98 toEntries: years, 99 }, 100 { 101 re: `^media/by-year/(\d{4})$`, 102 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (fs.DirEntries, error) { 103 filter, err := yearMonthDayFilter(ctx, f, match) 104 if err != nil { 105 return nil, err 106 } 107 return f.listDir(ctx, prefix, filter) 108 }, 109 }, 110 { 111 re: `^media/by-year/(\d{4})/([^/]+)$`, 112 isFile: true, 113 }, 114 { 115 re: `^media/by-month$`, 116 toEntries: years, 117 }, 118 { 119 re: `^media/by-month/(\d{4})$`, 120 toEntries: months, 121 }, 122 { 123 re: `^media/by-month/\d{4}/(\d{4})-(\d{2})$`, 124 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (fs.DirEntries, error) { 125 filter, err := yearMonthDayFilter(ctx, f, match) 126 if err != nil { 127 return nil, err 128 } 129 return f.listDir(ctx, prefix, filter) 130 }, 131 }, 132 { 133 re: `^media/by-month/\d{4}/(\d{4})-(\d{2})/([^/]+)$`, 134 isFile: true, 135 }, 136 { 137 re: `^media/by-day$`, 138 toEntries: years, 139 }, 140 { 141 re: `^media/by-day/(\d{4})$`, 142 toEntries: days, 143 }, 144 { 145 re: `^media/by-day/\d{4}/(\d{4})-(\d{2})-(\d{2})$`, 146 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (fs.DirEntries, error) { 147 filter, err := yearMonthDayFilter(ctx, f, match) 148 if err != nil { 149 return nil, err 150 } 151 return f.listDir(ctx, prefix, filter) 152 }, 153 }, 154 { 155 re: `^media/by-day/\d{4}/(\d{4})-(\d{2})-(\d{2})/([^/]+)$`, 156 isFile: true, 157 }, 158 { 159 re: `^album$`, 160 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 161 return albumsToEntries(ctx, f, false, prefix, "") 162 }, 163 }, 164 { 165 re: `^album/(.+)$`, 166 canMkdir: true, 167 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 168 return albumsToEntries(ctx, f, false, prefix, match[1]) 169 170 }, 171 }, 172 { 173 re: `^album/(.+?)/([^/]+)$`, 174 canUpload: true, 175 isFile: true, 176 }, 177 { 178 re: `^shared-album$`, 179 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 180 return albumsToEntries(ctx, f, true, prefix, "") 181 }, 182 }, 183 { 184 re: `^shared-album/(.+)$`, 185 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 186 return albumsToEntries(ctx, f, true, prefix, match[1]) 187 188 }, 189 }, 190 { 191 re: `^shared-album/(.+?)/([^/]+)$`, 192 isFile: true, 193 }, 194 { 195 re: `^feature$`, 196 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 197 return fs.DirEntries{ 198 fs.NewDir(prefix+"favorites", f.dirTime()), 199 }, nil 200 }, 201 }, 202 { 203 re: `^feature/favorites$`, 204 toEntries: func(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 205 filter := featureFilter(ctx, f, match) 206 if err != nil { 207 return nil, err 208 } 209 return f.listDir(ctx, prefix, filter) 210 }, 211 }, 212 { 213 re: `^feature/favorites/([^/]+)$`, 214 isFile: true, 215 }, 216 }.mustCompile() 217 218 // mustCompile compiles the regexps in the dirPatterns 219 func (ds dirPatterns) mustCompile() dirPatterns { 220 for i := range ds { 221 pattern := &ds[i] 222 pattern.match = regexp.MustCompile(pattern.re) 223 } 224 return ds 225 } 226 227 // match finds the path passed in the matching structure and 228 // returns the parameters and a pointer to the match, or nil. 229 func (ds dirPatterns) match(root string, itemPath string, isFile bool) (match []string, prefix string, pattern *dirPattern) { 230 itemPath = strings.Trim(itemPath, "/") 231 absPath := path.Join(root, itemPath) 232 prefix = strings.Trim(absPath[len(root):], "/") 233 if prefix != "" { 234 prefix += "/" 235 } 236 for i := range ds { 237 pattern = &ds[i] 238 if pattern.isFile != isFile { 239 continue 240 } 241 match = pattern.match.FindStringSubmatch(absPath) 242 if match != nil { 243 return 244 } 245 } 246 return nil, "", nil 247 } 248 249 // Return the years from startYear to today 250 func years(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 251 currentYear := f.dirTime().Year() 252 for year := f.startYear(); year <= currentYear; year++ { 253 entries = append(entries, fs.NewDir(prefix+fmt.Sprint(year), f.dirTime())) 254 } 255 return entries, nil 256 } 257 258 // Return the months in a given year 259 func months(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 260 year := match[1] 261 for month := 1; month <= 12; month++ { 262 entries = append(entries, fs.NewDir(fmt.Sprintf("%s%s-%02d", prefix, year, month), f.dirTime())) 263 } 264 return entries, nil 265 } 266 267 // Return the days in a given year 268 func days(ctx context.Context, f lister, prefix string, match []string) (entries fs.DirEntries, err error) { 269 year := match[1] 270 current, err := time.Parse("2006", year) 271 if err != nil { 272 return nil, fmt.Errorf("bad year %q", match[1]) 273 } 274 currentYear := current.Year() 275 for current.Year() == currentYear { 276 entries = append(entries, fs.NewDir(prefix+current.Format("2006-01-02"), f.dirTime())) 277 current = current.AddDate(0, 0, 1) 278 } 279 return entries, nil 280 } 281 282 // This creates a search filter on year/month/day as provided 283 func yearMonthDayFilter(ctx context.Context, f lister, match []string) (sf api.SearchFilter, err error) { 284 year, err := strconv.Atoi(match[1]) 285 if err != nil || year < 1000 || year > 3000 { 286 return sf, fmt.Errorf("bad year %q", match[1]) 287 } 288 sf = api.SearchFilter{ 289 Filters: &api.Filters{ 290 DateFilter: &api.DateFilter{ 291 Dates: []api.Date{ 292 { 293 Year: year, 294 }, 295 }, 296 }, 297 }, 298 } 299 if len(match) >= 3 { 300 month, err := strconv.Atoi(match[2]) 301 if err != nil || month < 1 || month > 12 { 302 return sf, fmt.Errorf("bad month %q", match[2]) 303 } 304 sf.Filters.DateFilter.Dates[0].Month = month 305 } 306 if len(match) >= 4 { 307 day, err := strconv.Atoi(match[3]) 308 if err != nil || day < 1 || day > 31 { 309 return sf, fmt.Errorf("bad day %q", match[3]) 310 } 311 sf.Filters.DateFilter.Dates[0].Day = day 312 } 313 return sf, nil 314 } 315 316 // featureFilter creates a filter for the Feature enum 317 // 318 // The API only supports one feature, FAVORITES, so hardcode that feature. 319 // 320 // https://developers.google.com/photos/library/reference/rest/v1/mediaItems/search#FeatureFilter 321 func featureFilter(ctx context.Context, f lister, match []string) (sf api.SearchFilter) { 322 sf = api.SearchFilter{ 323 Filters: &api.Filters{ 324 FeatureFilter: &api.FeatureFilter{ 325 IncludedFeatures: []string{ 326 "FAVORITES", 327 }, 328 }, 329 }, 330 } 331 return sf 332 } 333 334 // Turns an albumPath into entries 335 // 336 // These can either be synthetic directory entries if the album path 337 // is a prefix of another album, or actual files, or a combination of 338 // the two. 339 func albumsToEntries(ctx context.Context, f lister, shared bool, prefix string, albumPath string) (entries fs.DirEntries, err error) { 340 albums, err := f.listAlbums(ctx, shared) 341 if err != nil { 342 return nil, err 343 } 344 // Put in the directories 345 dirs, foundAlbumPath := albums.getDirs(albumPath) 346 if foundAlbumPath { 347 for _, dir := range dirs { 348 d := fs.NewDir(prefix+dir, f.dirTime()) 349 dirPath := path.Join(albumPath, dir) 350 // if this dir is an album add more special stuff 351 album, ok := albums.get(dirPath) 352 if ok { 353 count, err := strconv.ParseInt(album.MediaItemsCount, 10, 64) 354 if err != nil { 355 fs.Debugf(f, "Error reading media count: %v", err) 356 } 357 d.SetID(album.ID).SetItems(count) 358 } 359 entries = append(entries, d) 360 } 361 } 362 // if this is an album then return a filter to list it 363 album, foundAlbum := albums.get(albumPath) 364 if foundAlbum { 365 filter := api.SearchFilter{AlbumID: album.ID} 366 newEntries, err := f.listDir(ctx, prefix, filter) 367 if err != nil { 368 return nil, err 369 } 370 entries = append(entries, newEntries...) 371 } 372 if !foundAlbumPath && !foundAlbum && albumPath != "" { 373 return nil, fs.ErrorDirNotFound 374 } 375 return entries, nil 376 }