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