1package main
2
3import (
4	"fmt"
5	"io"
6	"io/ioutil"
7	"net/http"
8	"os"
9	"path/filepath"
10	"time"
11
12	"github.com/prasmussen/gdrive/auth"
13	"github.com/prasmussen/gdrive/cli"
14	"github.com/prasmussen/gdrive/drive"
15)
16
17const ClientId = "367116221053-7n0vf5akeru7on6o2fjinrecpdoe99eg.apps.googleusercontent.com"
18const ClientSecret = "1qsNodXNaWq1mQuBjUjmvhoO"
19const TokenFilename = "token_v2.json"
20const ClientCredentialsFilename = "client_id.json"
21const DefaultCacheFileName = "file_cache.json"
22
23var usingClientCredentialsFile = false
24
25func listHandler(ctx cli.Context) {
26	args := ctx.Args()
27	err := newDrive(args).List(drive.ListFilesArgs{
28		Out:         os.Stdout,
29		MaxFiles:    args.Int64("maxFiles"),
30		NameWidth:   args.Int64("nameWidth"),
31		Query:       args.String("query"),
32		SortOrder:   args.String("sortOrder"),
33		SkipHeader:  args.Bool("skipHeader"),
34		SizeInBytes: args.Bool("sizeInBytes"),
35		AbsPath:     args.Bool("absPath"),
36	})
37	checkErr(err)
38}
39
40func listChangesHandler(ctx cli.Context) {
41	args := ctx.Args()
42	err := newDrive(args).ListChanges(drive.ListChangesArgs{
43		Out:        os.Stdout,
44		PageToken:  args.String("pageToken"),
45		MaxChanges: args.Int64("maxChanges"),
46		Now:        args.Bool("now"),
47		NameWidth:  args.Int64("nameWidth"),
48		SkipHeader: args.Bool("skipHeader"),
49	})
50	checkErr(err)
51}
52
53func downloadHandler(ctx cli.Context) {
54	args := ctx.Args()
55	checkDownloadArgs(args)
56	err := newDrive(args).Download(drive.DownloadArgs{
57		Out:       os.Stdout,
58		Id:        args.String("fileId"),
59		Force:     args.Bool("force"),
60		Skip:      args.Bool("skip"),
61		Path:      args.String("path"),
62		Delete:    args.Bool("delete"),
63		Recursive: args.Bool("recursive"),
64		Stdout:    args.Bool("stdout"),
65		Progress:  progressWriter(args.Bool("noProgress")),
66		Timeout:   durationInSeconds(args.Int64("timeout")),
67	})
68	checkErr(err)
69}
70
71func downloadQueryHandler(ctx cli.Context) {
72	args := ctx.Args()
73	err := newDrive(args).DownloadQuery(drive.DownloadQueryArgs{
74		Out:       os.Stdout,
75		Query:     args.String("query"),
76		Force:     args.Bool("force"),
77		Skip:      args.Bool("skip"),
78		Recursive: args.Bool("recursive"),
79		Path:      args.String("path"),
80		Progress:  progressWriter(args.Bool("noProgress")),
81	})
82	checkErr(err)
83}
84
85func downloadSyncHandler(ctx cli.Context) {
86	args := ctx.Args()
87	cachePath := filepath.Join(args.String("configDir"), DefaultCacheFileName)
88	err := newDrive(args).DownloadSync(drive.DownloadSyncArgs{
89		Out:              os.Stdout,
90		Progress:         progressWriter(args.Bool("noProgress")),
91		Path:             args.String("path"),
92		RootId:           args.String("fileId"),
93		DryRun:           args.Bool("dryRun"),
94		DeleteExtraneous: args.Bool("deleteExtraneous"),
95		Timeout:          durationInSeconds(args.Int64("timeout")),
96		Resolution:       conflictResolution(args),
97		Comparer:         NewCachedMd5Comparer(cachePath),
98	})
99	checkErr(err)
100}
101
102func downloadRevisionHandler(ctx cli.Context) {
103	args := ctx.Args()
104	err := newDrive(args).DownloadRevision(drive.DownloadRevisionArgs{
105		Out:        os.Stdout,
106		FileId:     args.String("fileId"),
107		RevisionId: args.String("revId"),
108		Force:      args.Bool("force"),
109		Stdout:     args.Bool("stdout"),
110		Path:       args.String("path"),
111		Progress:   progressWriter(args.Bool("noProgress")),
112		Timeout:    durationInSeconds(args.Int64("timeout")),
113	})
114	checkErr(err)
115}
116
117func uploadHandler(ctx cli.Context) {
118	args := ctx.Args()
119	checkUploadArgs(args)
120	err := newDrive(args).Upload(drive.UploadArgs{
121		Out:         os.Stdout,
122		Progress:    progressWriter(args.Bool("noProgress")),
123		Path:        args.String("path"),
124		Name:        args.String("name"),
125		Description: args.String("description"),
126		Parents:     args.StringSlice("parent"),
127		Mime:        args.String("mime"),
128		Recursive:   args.Bool("recursive"),
129		Share:       args.Bool("share"),
130		Delete:      args.Bool("delete"),
131		ChunkSize:   args.Int64("chunksize"),
132		Timeout:     durationInSeconds(args.Int64("timeout")),
133	})
134	checkErr(err)
135}
136
137func uploadStdinHandler(ctx cli.Context) {
138	args := ctx.Args()
139	err := newDrive(args).UploadStream(drive.UploadStreamArgs{
140		Out:         os.Stdout,
141		In:          os.Stdin,
142		Name:        args.String("name"),
143		Description: args.String("description"),
144		Parents:     args.StringSlice("parent"),
145		Mime:        args.String("mime"),
146		Share:       args.Bool("share"),
147		ChunkSize:   args.Int64("chunksize"),
148		Timeout:     durationInSeconds(args.Int64("timeout")),
149		Progress:    progressWriter(args.Bool("noProgress")),
150	})
151	checkErr(err)
152}
153
154func uploadSyncHandler(ctx cli.Context) {
155	args := ctx.Args()
156	cachePath := filepath.Join(args.String("configDir"), DefaultCacheFileName)
157	err := newDrive(args).UploadSync(drive.UploadSyncArgs{
158		Out:              os.Stdout,
159		Progress:         progressWriter(args.Bool("noProgress")),
160		Path:             args.String("path"),
161		RootId:           args.String("fileId"),
162		DryRun:           args.Bool("dryRun"),
163		DeleteExtraneous: args.Bool("deleteExtraneous"),
164		ChunkSize:        args.Int64("chunksize"),
165		Timeout:          durationInSeconds(args.Int64("timeout")),
166		Resolution:       conflictResolution(args),
167		Comparer:         NewCachedMd5Comparer(cachePath),
168	})
169	checkErr(err)
170}
171
172func updateHandler(ctx cli.Context) {
173	args := ctx.Args()
174	err := newDrive(args).Update(drive.UpdateArgs{
175		Out:         os.Stdout,
176		Id:          args.String("fileId"),
177		Path:        args.String("path"),
178		Name:        args.String("name"),
179		Description: args.String("description"),
180		Parents:     args.StringSlice("parent"),
181		Mime:        args.String("mime"),
182		Progress:    progressWriter(args.Bool("noProgress")),
183		ChunkSize:   args.Int64("chunksize"),
184		Timeout:     durationInSeconds(args.Int64("timeout")),
185	})
186	checkErr(err)
187}
188
189func infoHandler(ctx cli.Context) {
190	args := ctx.Args()
191	err := newDrive(args).Info(drive.FileInfoArgs{
192		Out:         os.Stdout,
193		Id:          args.String("fileId"),
194		SizeInBytes: args.Bool("sizeInBytes"),
195	})
196	checkErr(err)
197}
198
199func importHandler(ctx cli.Context) {
200	args := ctx.Args()
201	err := newDrive(args).Import(drive.ImportArgs{
202		Mime:     args.String("mime"),
203		Out:      os.Stdout,
204		Path:     args.String("path"),
205		Parents:  args.StringSlice("parent"),
206		Progress: progressWriter(args.Bool("noProgress")),
207	})
208	checkErr(err)
209}
210
211func exportHandler(ctx cli.Context) {
212	args := ctx.Args()
213	err := newDrive(args).Export(drive.ExportArgs{
214		Out:        os.Stdout,
215		Id:         args.String("fileId"),
216		Mime:       args.String("mime"),
217		PrintMimes: args.Bool("printMimes"),
218		Force:      args.Bool("force"),
219	})
220	checkErr(err)
221}
222
223func listRevisionsHandler(ctx cli.Context) {
224	args := ctx.Args()
225	err := newDrive(args).ListRevisions(drive.ListRevisionsArgs{
226		Out:         os.Stdout,
227		Id:          args.String("fileId"),
228		NameWidth:   args.Int64("nameWidth"),
229		SizeInBytes: args.Bool("sizeInBytes"),
230		SkipHeader:  args.Bool("skipHeader"),
231	})
232	checkErr(err)
233}
234
235func mkdirHandler(ctx cli.Context) {
236	args := ctx.Args()
237	err := newDrive(args).Mkdir(drive.MkdirArgs{
238		Out:         os.Stdout,
239		Name:        args.String("name"),
240		Description: args.String("description"),
241		Parents:     args.StringSlice("parent"),
242	})
243	checkErr(err)
244}
245
246func shareHandler(ctx cli.Context) {
247	args := ctx.Args()
248	err := newDrive(args).Share(drive.ShareArgs{
249		Out:          os.Stdout,
250		FileId:       args.String("fileId"),
251		Role:         args.String("role"),
252		Type:         args.String("type"),
253		Email:        args.String("email"),
254		Domain:       args.String("domain"),
255		Discoverable: args.Bool("discoverable"),
256	})
257	checkErr(err)
258}
259
260func shareListHandler(ctx cli.Context) {
261	args := ctx.Args()
262	err := newDrive(args).ListPermissions(drive.ListPermissionsArgs{
263		Out:    os.Stdout,
264		FileId: args.String("fileId"),
265	})
266	checkErr(err)
267}
268
269func shareRevokeHandler(ctx cli.Context) {
270	args := ctx.Args()
271	err := newDrive(args).RevokePermission(drive.RevokePermissionArgs{
272		Out:          os.Stdout,
273		FileId:       args.String("fileId"),
274		PermissionId: args.String("permissionId"),
275	})
276	checkErr(err)
277}
278
279func deleteHandler(ctx cli.Context) {
280	args := ctx.Args()
281	err := newDrive(args).Delete(drive.DeleteArgs{
282		Out:       os.Stdout,
283		Id:        args.String("fileId"),
284		Recursive: args.Bool("recursive"),
285	})
286	checkErr(err)
287}
288
289func listSyncHandler(ctx cli.Context) {
290	args := ctx.Args()
291	err := newDrive(args).ListSync(drive.ListSyncArgs{
292		Out:        os.Stdout,
293		SkipHeader: args.Bool("skipHeader"),
294	})
295	checkErr(err)
296}
297
298func listRecursiveSyncHandler(ctx cli.Context) {
299	args := ctx.Args()
300	err := newDrive(args).ListRecursiveSync(drive.ListRecursiveSyncArgs{
301		Out:         os.Stdout,
302		RootId:      args.String("fileId"),
303		SkipHeader:  args.Bool("skipHeader"),
304		PathWidth:   args.Int64("pathWidth"),
305		SizeInBytes: args.Bool("sizeInBytes"),
306		SortOrder:   args.String("sortOrder"),
307	})
308	checkErr(err)
309}
310
311func deleteRevisionHandler(ctx cli.Context) {
312	args := ctx.Args()
313	err := newDrive(args).DeleteRevision(drive.DeleteRevisionArgs{
314		Out:        os.Stdout,
315		FileId:     args.String("fileId"),
316		RevisionId: args.String("revId"),
317	})
318	checkErr(err)
319}
320
321func aboutHandler(ctx cli.Context) {
322	args := ctx.Args()
323	err := newDrive(args).About(drive.AboutArgs{
324		Out:         os.Stdout,
325		SizeInBytes: args.Bool("sizeInBytes"),
326	})
327	checkErr(err)
328}
329
330func aboutImportHandler(ctx cli.Context) {
331	args := ctx.Args()
332	err := newDrive(args).AboutImport(drive.AboutImportArgs{
333		Out: os.Stdout,
334	})
335	checkErr(err)
336}
337
338func aboutExportHandler(ctx cli.Context) {
339	args := ctx.Args()
340	err := newDrive(args).AboutExport(drive.AboutExportArgs{
341		Out: os.Stdout,
342	})
343	checkErr(err)
344}
345
346func getOauthClient(args cli.Arguments) (*http.Client, error) {
347	if args.String("refreshToken") != "" && args.String("accessToken") != "" {
348		ExitF("Access token not needed when refresh token is provided")
349	}
350
351	configDir := getConfigDir(args)
352
353	clientCredentialsPath := ConfigFilePath(configDir, ClientCredentialsFilename)
354	clientCredentials, exists, err := auth.ReadClientCredentials(clientCredentialsPath)
355	if err != nil {
356		ExitF("Failed to read client credentials file: %s", err)
357	} else if !exists {
358		clientCredentials = auth.AssembleClientCredentials(ClientId, ClientSecret)
359	} else {
360		usingClientCredentialsFile = true
361		// Make sure the google drive scope is present
362		if len(clientCredentials.Scopes) == 0 {
363			clientCredentials.Scopes = append(clientCredentials.Scopes, "https://www.googleapis.com/auth/drive")
364		}
365	}
366
367
368	if args.String("refreshToken") != "" {
369		return auth.NewRefreshTokenClient(clientCredentials, args.String("refreshToken")), nil
370	}
371
372	if args.String("accessToken") != "" {
373		return auth.NewAccessTokenClient(clientCredentials, args.String("accessToken")), nil
374	}
375
376	if args.String("serviceAccount") != "" {
377		serviceAccountPath := ConfigFilePath(configDir, args.String("serviceAccount"))
378		serviceAccountClient, err := auth.NewServiceAccountClient(serviceAccountPath)
379		if err != nil {
380			return nil, err
381		}
382		return serviceAccountClient, nil
383	}
384
385	tokenPath := ConfigFilePath(configDir, TokenFilename)
386	return auth.NewFileSourceClient(clientCredentials, tokenPath, authCodePrompt)
387}
388
389func getConfigDir(args cli.Arguments) string {
390	// Use dir from environment var if present
391	if os.Getenv("GDRIVE_CONFIG_DIR") != "" {
392		return os.Getenv("GDRIVE_CONFIG_DIR")
393	}
394	return args.String("configDir")
395}
396
397func newDrive(args cli.Arguments) *drive.Drive {
398	oauth, err := getOauthClient(args)
399	if err != nil {
400		ExitF("Failed getting oauth client: %s", err.Error())
401	}
402
403	client, err := drive.New(oauth)
404	if err != nil {
405		ExitF("Failed getting drive: %s", err.Error())
406	}
407
408	return client
409}
410
411func authCodePrompt(url string) func() string {
412	return func() string {
413		if (usingClientCredentialsFile == true) {
414			fmt.Println("Client credentials loaded from file\n")
415		} else {
416			fmt.Println("Client credentials file not found. Using built-in defaults\n")
417		}
418
419		fmt.Println("Authentication needed")
420		fmt.Println("Go to the following url in your browser:")
421		fmt.Printf("%s\n\n", url)
422		fmt.Print("Enter verification code: ")
423
424		var code string
425		if _, err := fmt.Scan(&code); err != nil {
426			fmt.Printf("Failed reading code: %s", err.Error())
427		}
428		return code
429	}
430}
431
432func progressWriter(discard bool) io.Writer {
433	if discard {
434		return ioutil.Discard
435	}
436	return os.Stderr
437}
438
439func durationInSeconds(seconds int64) time.Duration {
440	return time.Second * time.Duration(seconds)
441}
442
443func conflictResolution(args cli.Arguments) drive.ConflictResolution {
444	keepLocal := args.Bool("keepLocal")
445	keepRemote := args.Bool("keepRemote")
446	keepLargest := args.Bool("keepLargest")
447
448	if (keepLocal && keepRemote) || (keepLocal && keepLargest) || (keepRemote && keepLargest) {
449		ExitF("Only one conflict resolution flag can be given")
450	}
451
452	if keepLocal {
453		return drive.KeepLocal
454	}
455
456	if keepRemote {
457		return drive.KeepRemote
458	}
459
460	if keepLargest {
461		return drive.KeepLargest
462	}
463
464	return drive.NoResolution
465}
466
467func checkUploadArgs(args cli.Arguments) {
468	if args.Bool("recursive") && args.Bool("delete") {
469		ExitF("--delete is not allowed for recursive uploads")
470	}
471
472	if args.Bool("recursive") && args.Bool("share") {
473		ExitF("--share is not allowed for recursive uploads")
474	}
475}
476
477func checkDownloadArgs(args cli.Arguments) {
478	if args.Bool("recursive") && args.Bool("delete") {
479		ExitF("--delete is not allowed for recursive downloads")
480	}
481}
482