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  }