1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17 package gphotos
18
19 import (
20 "context"
21 "errors"
22 "io"
23 "log"
24 "net/http"
25 "strings"
26 "time"
27
28 "golang.org/x/time/rate"
29 "google.golang.org/api/drive/v3"
30 "google.golang.org/api/googleapi"
31 )
32
33 var scopeURLs = []string{drive.DriveReadonlyScope}
34
35 const (
36
37 batchSize = 1000
38
39
40
41
42
43 defaultRateLimit = rate.Limit(10)
44 )
45
46
47
48
49
50
51
52 func getUser(ctx context.Context, client *http.Client) (*drive.User, error) {
53 srv, err := drive.New(client)
54 if err != nil {
55 return nil, err
56 }
57 about, err := srv.About.Get().
58 Context(ctx).
59 Fields("user(displayName,emailAddress,permissionId)").Do()
60 if err != nil {
61 return nil, err
62 }
63 return about.User, nil
64 }
65
66 type downloader struct {
67
68 rate *rate.Limiter
69
70 *drive.Service
71 }
72
73
74
75
76
77
78 func newDownloader(client *http.Client) (*downloader, error) {
79 srv, err := drive.New(client)
80 if err != nil {
81 return nil, err
82 }
83 return &downloader{
84 rate: rate.NewLimiter(defaultRateLimit, 1),
85 Service: srv,
86 }, nil
87 }
88
89
90
91
92
93
94
95
96
97
98 func (dl *downloader) foreachPhoto(ctx context.Context, sinceToken string, fn func(context.Context, *photo) error) (nextToken string, err error) {
99
100 if sinceToken != "" {
101 return dl.foreachPhotoFromChanges(ctx, sinceToken, fn)
102 }
103
104
105
106
107 var sr *drive.StartPageToken
108 if err := dl.rateLimit(ctx, func() error {
109 var err error
110 sr, err = dl.Service.Changes.GetStartPageToken().Do()
111 return err
112 }); err != nil {
113 return "", err
114 }
115 nextToken = sr.StartPageToken
116 if nextToken == "" {
117 return "", errors.New("unexpected gdrive Changes.GetStartPageToken response with empty StartPageToken")
118 }
119
120 if err := dl.foreachPhotoFromScratch(ctx, fn); err != nil {
121 return "", err
122 }
123 return nextToken, nil
124 }
125
126 const fields = "id,name,size,spaces,mimeType,description,starred,properties,version,webContentLink,createdTime,modifiedTime,originalFilename,imageMediaMetadata(location,time)"
127
128 func (dl *downloader) foreachPhotoFromScratch(ctx context.Context, fn func(context.Context, *photo) error) error {
129 var token string
130 for {
131 select {
132 case <-ctx.Done():
133 return ctx.Err()
134 default:
135 }
136
137 var r *drive.FileList
138 if err := dl.rateLimit(ctx, func() error {
139 var err error
140 listCall := dl.Service.Files.List().
141 Context(ctx).
142 Fields("nextPageToken, files(" + fields + ")").
143
144
145
146
147
148 OrderBy("createdTime desc,folder").
149
150
151
152
153 Spaces("drive").
154 PageSize(batchSize).
155 PageToken(token)
156 r, err = listCall.Do()
157 return err
158 }); err != nil {
159 return err
160 }
161
162 logf("got gdrive API response of batch of %d files", len(r.Files))
163 for _, f := range r.Files {
164 if f == nil {
165
166 logf("unexpected nil entry in gdrive file list response")
167 continue
168 }
169 ph := dl.fileAsPhoto(f)
170 if ph == nil {
171
172 continue
173 }
174 if err := fn(ctx, ph); err != nil {
175 return err
176 }
177 }
178 token = r.NextPageToken
179 if token == "" {
180 return nil
181 }
182 }
183 }
184
185 func (dl *downloader) foreachPhotoFromChanges(ctx context.Context, sinceToken string, fn func(context.Context, *photo) error) (nextToken string, err error) {
186 token := sinceToken
187 for {
188 select {
189 case <-ctx.Done():
190 return "", err
191 default:
192 }
193
194 var r *drive.ChangeList
195 if err := dl.rateLimit(ctx, func() error {
196 logf("importing changes from token point %q", token)
197 var err error
198 r, err = dl.Service.Changes.List(token).
199 Context(ctx).
200 Fields("nextPageToken,newStartPageToken, changes(file(" + fields + "))").
201
202
203
204
205 Spaces("drive").
206 PageSize(batchSize).
207 RestrictToMyDrive(true).
208 IncludeRemoved(false).Do()
209 return err
210 }); err != nil {
211 return "", err
212 }
213 for _, c := range r.Changes {
214 if c.File == nil {
215
216 logf("unexpected nil entry in gdrive changes response")
217 continue
218 }
219 ph := dl.fileAsPhoto(c.File)
220 if ph == nil {
221
222 continue
223 }
224 if err := fn(ctx, ph); err != nil {
225 return "", err
226 }
227 }
228 token = r.NextPageToken
229 if token == "" {
230 nextToken = r.NewStartPageToken
231 if nextToken == "" {
232 return "", errors.New("unexpected gdrive changes response with both NextPageToken and NewStartPageToken empty")
233 }
234 return nextToken, nil
235 }
236 }
237 }
238
239 type photo struct {
240 ID string
241 Name, MimeType, Description string
242 Starred bool
243 Properties map[string]string
244 WebContentLink string
245 CreatedTime, ModifiedTime time.Time
246 OriginalFilename string
247 Version int64
248 drive.FileImageMediaMetadata
249 }
250
251 func (dl *downloader) openPhoto(ctx context.Context, photo photo) (io.ReadCloser, error) {
252 logf("importing media from %v", photo.WebContentLink)
253 var resp *http.Response
254 err := dl.rateLimit(ctx, func() error {
255 var err error
256 resp, err = dl.Service.Files.Get(photo.ID).Context(ctx).Download()
257 return err
258 })
259 if err != nil {
260 return nil, err
261 }
262 return resp.Body, err
263 }
264
265
266
267 func inPhotoSpace(f *drive.File) bool {
268 for _, v := range f.Spaces {
269 if v == "photos" {
270 return true
271 }
272 }
273 return false
274 }
275
276
277
278
279
280
281 func (dl *downloader) fileAsPhoto(f *drive.File) *photo {
282 if f == nil {
283 return nil
284 }
285 if f.Size == 0 {
286
287 return nil
288 }
289 if !inPhotoSpace(f) {
290
291 return nil
292 }
293 p := &photo{
294 ID: f.Id,
295 Name: f.Name,
296 Starred: f.Starred,
297 Version: f.Version,
298 MimeType: f.MimeType,
299 Properties: f.Properties,
300 Description: f.Description,
301 WebContentLink: f.WebContentLink,
302 OriginalFilename: f.OriginalFilename,
303 }
304 if f.ImageMediaMetadata != nil {
305 p.FileImageMediaMetadata = *f.ImageMediaMetadata
306 }
307 if f.CreatedTime != "" {
308 p.CreatedTime, _ = time.Parse(time.RFC3339, f.CreatedTime)
309 }
310 if f.ModifiedTime != "" {
311 p.ModifiedTime, _ = time.Parse(time.RFC3339, f.ModifiedTime)
312 }
313
314 return p
315 }
316
317
318
319 func (dl *downloader) rateLimit(ctx context.Context, f func() error) error {
320 const (
321 msgRateLimitExceeded = "Rate Limit Exceeded"
322 msgUserRateLimitExceeded = "User Rate Limit Exceeded"
323 msgUserRateLimitExceededShort = "userRateLimitExceeded"
324 )
325
326
327 ctx, cancel := context.WithTimeout(ctx, time.Minute)
328 defer cancel()
329 for {
330 if err := dl.rate.Wait(ctx); err != nil {
331 log.Printf("gphotos: rate limit failure: %v", err)
332 return err
333 }
334 err := f()
335 if err == nil {
336 return nil
337 }
338 ge, ok := err.(*googleapi.Error)
339 if !ok || ge.Code != http.StatusForbidden {
340 return err
341 }
342 if ge.Message == "" {
343 var ok bool
344 for _, e := range ge.Errors {
345 if ok = e.Reason == msgUserRateLimitExceededShort; ok {
346 break
347 }
348 }
349
350
351
352
353
354
355 if !ok && !strings.Contains(ge.Body, msgRateLimitExceeded) {
356 return err
357 }
358 }
359
360 log.Printf("gphotos: sleeping for 5s after 403 error, presumably due to a rate limit")
361 time.Sleep(5 * time.Second)
362 log.Printf("gphotos: retrying after sleep...")
363 }
364 }