0
\$\begingroup\$

I am trying to retrieve filename and ID list with Google Drive API (the latest Drive API version v3 is used here) as a first step in order to analyze and manipulate these files on Google Drive. The experimental implementation is as below. (Note: credentials.json file is needed and the step to generate this file is here. )

The experimental implementation

using Google.Apis.Auth.OAuth2;
using Google.Apis.Drive.v3;
using Google.Apis.Drive.v3.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace GoogleDriveFileManager
{
    class Program
    {
        static string[] Scopes = { DriveService.Scope.DriveReadonly };
        static string ApplicationName = "GoogleDriveFileManager";

        static void Main(string[] args)
        {
            UserCredential credential;

            using (var stream =
                new FileStream("credentials.json", FileMode.Open, FileAccess.Read))
            {
                string credPath = "token.json";
                credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.Load(stream).Secrets,
                    Scopes,
                    "user",
                    CancellationToken.None,
                    new FileDataStore(credPath, true)).Result;
                Console.WriteLine($"Credential file saved to: {credPath}");
            }

            // Create Drive API service.
            var service = new DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            });

            // Define parameters of request.
            FilesResource.ListRequest listRequest = service.Files.List();
            listRequest.PageSize = 1000;
            listRequest.Fields = "nextPageToken, files(id, name)";

            // List files.
            IList<Google.Apis.Drive.v3.Data.File> files = listRequest.Execute()
                .Files;
            Console.WriteLine("Files:");
            Console.WriteLine($"file.Name{new String('\t', 6)}file.Id{new String('\t', 8)}file.ModifiedTime");
            if (files != null && files.Count > 0)
            {
                foreach (var file in files)
                {
                    Console.WriteLine(
                        $"{file.Name.PadRight(56)}{file.Id.PadRight(36)}{file.ModifiedTime}"
                        );
                }
            }
            else
            {
                Console.WriteLine("No files found.");
            }
            Console.Read();
        }
    }
}

If there is any possible improvement about potential drawback or unnecessary overhead, please let me know.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Which version of C# are you using? I'm asking it because for instance since C# 7.1 async main is supported. \$\endgroup\$ Apr 13, 2021 at 7:35

1 Answer 1

1
\$\begingroup\$

First of all let me clarify that I'm not familiar with the Google Drive API. I've done some refactoring but I haven't tested it (just make it compile).

If I can assume you are using greater C# version than 8 then you can refactor your code like this:

using static {YourMainNamespace}.Constants;

class Program
{
    static async Task Main(string[] args)
    {
        UserCredential credential = await AuthorizeAgainstGoogleDriveAsync();
        IList<Google.Apis.Drive.v3.Data.File> files = await GetFilesMetaInfoAsync(credential);
        DisplayFilesMetaInfoInTabularFashion(files);
        Console.ReadLine();
    }

    static async Task<UserCredential> AuthorizeAgainstGoogleDriveAsync(
        string sourceRelativePath = Authorize.RelativeFilePath.Source,
        string targetRelativePath = Authorize.RelativeFilePath.Target)
    {
        using var stream = new FileStream(sourceRelativePath, FileMode.Open, FileAccess.Read);

        var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
            GoogleClientSecrets.Load(stream).Secrets,
            Authorize.Scopes,
            Authorize.User,
            CancellationToken.None,
            new FileDataStore(targetRelativePath, true));

        Console.WriteLine($"Credential file saved to: {Authorize.RelativeFilePath.Target}");

        return credential;
    }

    static async Task<IList<Google.Apis.Drive.v3.Data.File>> GetFilesMetaInfoAsync(
        UserCredential credential,
        string applicationName = GetFiles.ApplicationName,
        int pageSize = GetFiles.PageSize,
        string fields = GetFiles.Fields)
    {
        var service = new DriveService(new BaseClientService.Initializer()
        {
            HttpClientInitializer = credential,
            ApplicationName = applicationName,
        });

        FilesResource.ListRequest listRequest = service.Files.List();
        listRequest.PageSize = pageSize;
        listRequest.Fields = fields;

        var fileList = await listRequest.ExecuteAsync();

        return fileList.Files;
    }

    static void DisplayFilesMetaInfoInTabularFashion(IList<Google.Apis.Drive.v3.Data.File> files)
    {
        Console.WriteLine("Files:");
        Console.WriteLine($"{DisplayFiles.Name}{new string('\t', 6)}{DisplayFiles.Id}{new string('\t', 8)}{DisplayFiles.ModifiedTime}");

        if (files == null || files.Count <= 0)
        {
            Console.WriteLine(DisplayFiles.NoFileMessage);
            return;
        }

        foreach (var file in files)
        {
            Console.WriteLine($"{file.Name,-56}{file.Id,-36}{file.ModifiedTime}");
        }
            
    }
}

static class Constans
{
    public static class Authorize
    {
        public const string User = "user";
        public readonly static string[] Scopes = new[] { DriveService.Scope.DriveReadonly };

        public static class RelativeFilePath
        {
            public const string Source = "credentials.json";
            public const string Target = "token.json";
        }
    }

    public static class GetFiles
    {
        public const string ApplicationName = "GoogleDriveFileManager";
        public const int PageSize = 1000;
        public const string Fields = "nextPageToken, files(id, name)";
    }

    public static class DisplayFiles
    {
        private const string prefix = "file.";
        public const string Name = prefix + nameof(Name);
        public const string Id = prefix + nameof(Id);
        public const string ModifiedTime = prefix + nameof(ModifiedTime);
        public const string NoFileMessage = "No files found.";
    }
}

Let me give you some context for each part:

Main

static async Task Main(string[] args)
{
    UserCredential credential = await AuthorizeAgainstGoogleDriveAsync();
    IList<Google.Apis.Drive.v3.Data.File> files = await GetFilesMetaInfoAsync(credential);
    DisplayFilesMetaInfoInTabularFashion(files);
    Console.ReadLine();
}
  • I've made your Main async (supported since C# 7.1) to be able to use async-await
  • I've split your logic into three smaller functions to make your Main short and concise
    • If the scope of the application is this small then you can use shorter names of course, like
      • AuthorizeAsync
      • GetFiles
      • DisplayFiles

Constants

using static {YourMainNamespace}.Constants;

...

static class Constans
{
    public static class Authorize
    {
        public const string User = "user";
        public readonly static ImmutableArray<string> Scopes = new[] { DriveService.Scope.DriveReadonly }.ToImmutableArray();

        public static class RelativeFilePath
        {
            public const string Source = "credentials.json";
            public const string Target = "token.json";
        }
    }

    public static class GetFiles
    {
        public const string ApplicationName = "GoogleDriveFileManager";
        public const int PageSize = 1000;
        public const string Fields = "nextPageToken, files(id, name)";
    }

    public static class DisplayFiles
    {
        private const string prefix = "file.";
        public const string Name = prefix + nameof(Name);
        public const string Id = prefix + nameof(Id);
        public const string ModifiedTime = prefix + nameof(ModifiedTime);
        public const string NoFileMessage = "No files found.";
    }
}
  • I've put all of the hardcoded values into a hierarchical structure
    • Because I'm unfamiliar with the domain I've organize them based on the usage
    • In normal cases you should organize them based on their semantics
  • I've used const (which is implicit static) to prevent modification
  • In case of Scope I've used ImmutableArray to prevent modification
    • Here I can't use const but it could (and should) be marked as readonly
  • I've used using static to avoid prefixing with Constants each and every time

AuthorizeAgainstGoogleDriveAsync

static async Task<UserCredential> AuthorizeAgainstGoogleDriveAsync(
    string sourceRelativePath = Authorize.RelativeFilePath.Source,
    string targetRelativePath = Authorize.RelativeFilePath.Target)
{
    using var stream = new FileStream(sourceRelativePath, FileMode.Open, FileAccess.Read);

    var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
        GoogleClientSecrets.Load(stream).Secrets,
        Authorize.Scopes,
        Authorize.User,
        CancellationToken.None,
        new FileDataStore(targetRelativePath, true));

    Console.WriteLine($"Credential file saved to: {Authorize.RelativeFilePath.Target}");

    return credential;
}
  • I've made the source and target file paths configurable (but with default values)
    • If you wish you can make the same with user and scopes
  • One can argue whether the Console.WriteLine should belong here or inside the Main
    • I've put it here to make the main as concise as possible
    • But from reusability point of view it should not belong here
  • I've used using declaration (which is a C# 8 feature) to avoid the block operator ({ ... })

GetFilesMetaInfoAsync

static async Task<IList<Google.Apis.Drive.v3.Data.File>> GetFilesMetaInfoAsync(
    UserCredential credential,
    string applicationName = GetFiles.ApplicationName,
    int pageSize = GetFiles.PageSize,
    string fields = GetFiles.Fields)
{
    var service = new DriveService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = credential,
        ApplicationName = applicationName,
    });

    FilesResource.ListRequest listRequest = service.Files.List();
    listRequest.PageSize = pageSize;
    listRequest.Fields = fields;

    var fileList = await listRequest.ExecuteAsync();

    return fileList.Files;
}
  • I've made this API call to async (Execute >> ExecuteAsync) to make use of the async non-blocking I/O
  • I've declared the function with three optional parameters to be able to customize the request if needed
  • I've realized that you are not making use of the nextPageToken but I did not remove it
    • I've assumed that the implementation of paging will be your next step

DisplayFilesMetaInfoInTabularFashion

static void DisplayFilesMetaInfoInTabularFashion(IList<Google.Apis.Drive.v3.Data.File> files)
{
    Console.WriteLine("Files:");
    Console.WriteLine($"{DisplayFiles.Name}{new string('\t', 6)}{DisplayFiles.Id}{new string('\t', 8)}{DisplayFiles.ModifiedTime}");

    if (files == null || files.Count <= 0)
    {
        Console.WriteLine(DisplayFiles.NoFileMessage);
        return;
    }

    foreach (var file in files)
    {
        Console.WriteLine($"{file.Name,-56}{file.Id,-36}{file.ModifiedTime}");
    } 
}
  • This operation is tightly coupled to the request
    • It highly relies on the retrieved fields
    • So, it might make sense to it make more dynamic if needed
  • I've used new string instead of new String
  • I've reverted the if statement to use the early exit pattern
    • With this the main logic (foreach) is not indented
  • I've also used the capabilities of string interpolation to avoid calls like PadRight
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.