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  }