Использование Google Drive в приложении C #

Допустим, хранилище является необходимой частью нашего приложения, но наш хостинг имеет ограниченный размер хранилища, и нам действительно нужно несколько гигабайт хранилища как можно дешевле. Итак, давайте создадим сервис, который сможет получать доступ и работать с Google Drive.

Необходимые условия

  • включен  Google Drive API в консоли Google Cloud;
  • зарегистрирован проект;
  • создан клиент OAuth 2.0 с разрешениями API-интерфейса Google drive;
  • установлен пакет nuget для Google drive v3 https://www.nuget.org/packages/Google.Apis.Drive.v3/ .

Начнем с создания пустого класса

public class DriveApiService
{

}

Аутентификация

У нас есть 2 варианта, чтобы получить доступ к хранилищу диска

  1. учетная запись службы
  2. учетная запись пользователя

Если вы не хотите, чтобы пользовательское взаимодействие и авторизация использовали учетную запись службы, у этой учетной записи есть недостатки: вы не можете получить доступ к интерфейсу Google Drive для этой учетной записи и не можете купить дополнительное хранилище для этой учетной записи.

Второй вариант — использовать наш личный аккаунт, но для этого потребуется однократный ручной вход пользователя. Мы будем использовать второй вариант. Поэтому в конструкторе сервиса мы будем обрабатывать авторизацию с помощью файла client_id.json, который генерируется для клиента Google OAuth.

public class DriveApiService
{
        protected static string[] scopes = { DriveService.Scope.Drive };
        protected readonly UserCredential credential;
        static string ApplicationName = "Название приложения";
        protected readonly DriveService service;
        protected readonly FileExtensionContentTypeProvider fileExtensionProvider;

        public DriveApiService()
        {
            using (var stream =
                new FileStream("client_id.json", FileMode.Open, FileAccess.Read))
            {
                string credPath = "token.json";

                credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.Load(stream).Secrets,
                    scopes,
                    ВАШ_АДРЕС_ЭЛ_ПОЧТЫ, // Используйте константу или читайте из файла настроек
                    CancellationToken.None,
                    new FileDataStore(credPath, true)).Result;

                fileExtensionProvider = new FileExtensionContentTypeProvider();
            }

            service = new DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            });
        }
}

Давайте попробуем это.
Давайте используем внедрение зависимостей и зарегистрируем ваш сервис как одиночный в нашем приложении.

services.AddSingleton<DriveApiService>();

После того, как вы запустите приложение, откроется окно браузера и вам потребуется войти в свою учетную запись Google. Если вход прошел успешно, в вашем приложении будет создан постоянный файл token.json, который будет содержать токен доступа и обновления для доступа к Google Drive API . Вам не нужно будет входить в систему до тех пор, пока созданный файл лежит рядом с приложением, а ваш токен обновления действителен. Токен без ограничения по времени, но если вы измените свой пароль учетной записи Google, он будет отозван, и вам придется снова войти в систему вручную.

Список файлов

Диск Google не имеет структуры каталогов (вы не можете использовать путь для навигации «root / parent / child.type»), он использует родительские дочерние отношения, один дочерний элемент может быть в нескольких родителях. В нашем приложении мы храним эти отношения в БД и эмулируем классическое поведение каталога со всеми его ограничениями, и мы фактически используем пути, но эта статья не будет охватывать эту часть.

Чтобы получить файлы в каталоге, нам нужно знать его идентификатор. Мы можем получить доступ к корневому каталогу с помощью ключевого слова root.

public IList<Google.Apis.Drive.v3.Data.File> ListEntities(string id = "root")
        {
            FilesResource.ListRequest listRequest = service.Files.List();
            listRequest.PageSize = 100;
            listRequest.Fields = "nextPageToken, files(id, name, parents, createdTime, modifiedTime, mimeType)";
            listRequest.Q = $"'{id}' in parents";

            return listRequest.Execute().Files;
        }

Метод ListEntities возвращает список файлов со всеми определенными полями, которые находятся в каталоге с заданным идентификатором. Если идентификатор не установлен, мы используем корневое ключевое слово по умолчанию. Новая папка должна иметь имя, тип mime и определенный родительский элемент. Будьте осторожны, Google диск разрешает несколько файлов / папок с одним и тем же именем (мы обрабатываем это на уровне базы данных приложения, так что мы заботимся :))

Создание папок

Для группировки файлов мы используем папки. Создание папок просто.

public Google.Apis.Drive.v3.Data.File CreateFolder(string name,string id = "root")
        {
            var fileMetadata = new Google.Apis.Drive.v3.Data.File()
            {
                Name = name,
                MimeType = "application/vnd.google-apps.folder",
                Parents = new[] { id }
            };

            var request = service.Files.Create(fileMetadata);
            request.Fields = "id, name, parents, createdTime, modifiedTime, mimeType";

            return requ

Если указан идентификатор родителя, мы создаем папку в определенном родителе, если нет, то мы создаем папку в корне. Ответ будет содержать все определенные поля.

Загрузка файла

Загрузка файла аналогична созданию папки.

public async Task<Google.Apis.Drive.v3.Data.File> Upload(IFormFile file, string documentId)
        {
            var name = ($"{DateTime.UtcNow.ToString()}.{Path.GetExtension(file.FileName)}");
            var mimeType = file.ContentType;

            var fileMetadata = new Google.Apis.Drive.v3.Data.File()
            {
                Name = name,
                MimeType = mimeType,
                Parents = new[] { documentId }
            };

            FilesResource.CreateMediaUpload request;
            using (var stream = file.OpenReadStream())
            {
                request = service.Files.Create(
                    fileMetadata, stream, mimeType);
                request.Fields = "id, name, parents, createdTime, modifiedTime, mimeType, thumbnailLink";
                await request.UploadAsync();
            }


            return request.ResponseBody;
        }

Сначала мы генерируем новое уникальное имя файла для нашего файла с правильным расширением, мы извлекаем правильный тип MIME из нашего файла и заполняем метаданные нашего файла. Затем мы создаем запрос на загрузку и загружаем наш поток. Ответ будет содержать все определенные поля.

Переименование файлов / папок

В нашем приложении мы можем редактировать только имя файла или папки, но, следуя этому примеру, вы можете изменить что-либо в объекте файла.

public void Rename(string name, string id)
        {
            Google.Apis.Drive.v3.Data.File file = service.Files.Get(id).Execute();

            var update = new Google.Apis.Drive.v3.Data.File();
            update.Name = name;

            service.Files.Update(update, id).Execute();
        }

Сначала мы получаем наш файл по указанному идентификатору, затем создаем запрос на обновление с метаданными, которые мы хотим изменить. Если вы хотите, вы могли бы реализовать некоторую обработку ошибок, потому что, если файл с данным идентификатором не существует, то он выдает ошибку (у нас есть обработка ошибок на верхнем уровне).

Удаление файла / папки

Удаление сущностей является прямым.

public void Remove(string id)
{
        service.Files.Delete(id).Execute();
}

Скачивание файла

Мы можем реализовать загрузку файла, которая загружает файл по его идентификатору, мы храним его содержимое в потоке памяти, устанавливаем его положение равным 0, чтобы мы могли работать с ним на верхних уровнях, а затем возвращаем поток.

 public async Task<Stream> Download(string fileId)
        {
                Stream outputstream = new MemoryStream();
                var request = service.Files.Get(fileId);

                await request.DownloadAsync(outputstream);

            outputstream.Position = 0;

                return outputstream;
        }

Бонус: мултискачивание — архивируем и качаем.

Сначала мы реализуем модель DTO, которая описывает наш файл.

public class ZipVM
    {
        public string Name { get; set; }
        public string DriveId { get; set; }
    }

Сначала мы реализуем модель DTO, которая описывает наш файл. Мы храним собственные имена файлов в нашей базе данных, на диске мы используем имена файлов на основе даты, чтобы иметь правильные имена файлов в сгенерированном zip-файле, поэтому нам нужно предоставить правильные имена файлов в метод, если вам это не нужно, вы можете использовать простой список идентификаторов или вы можете использовать метод Download.

Обработка загрузки zip-элемента аналогична загрузке одного файла, но этот метод работает с нашей моделью DTO и возвращает кортеж, состоящий из потока и имени файла. Зачем нам это нужно? На самом деле Google Drive API не предоставляет ничего для загрузки нескольких файлов одновременно.

ublic async Task<(Stream,string)> DownloadZipItem(ZipVM vm)
        {
            Stream outputstream = new MemoryStream();
            var request = service.Files.Get(vm.DriveId);

            await request.DownloadAsync(outputstream);

            outputstream.Position = 0;

            return (outputstream, vm.Name);
        }

Метод Zip принимает список DTO, которые мы определили. Затем мы создаем список задач DownloadZipItem из предоставленных DTO, которые мы вызываем параллельно. Затем мы создаем архив и возвращаем его как байтовый массив.

public async Task<byte[]> Zip(List<ZipVM> documents)
        {
            List<Task<(Stream,string)>> downloadTasks = new List<Task<(Stream,string)>>();

            for(int i = 0; i<documents.Count; i++)
            {
                downloadTasks.Add(DownloadZipItem(documents[i]));
            }

            await Task.WhenAll(downloadTasks);


            List<(Stream, string)> files = downloadTasks.Select(t => t.Result).ToList();

            byte[] archiveFile;
            using (var archiveStream = new MemoryStream())
            {
                using (var archive = new ZipArchive(archiveStream, ZipArchiveMode.Create, true))
                {
                    int i = 0;
                    foreach (var file in files)
                    {
                        var zipArchiveEntry = archive.CreateEntry(file.Item2, CompressionLevel.Fastest);
                        using (var zipStream = zipArchiveEntry.Open())
                            file.Item1.CopyTo(zipStream);
                    }
                }

                archiveFile = archiveStream.ToArray();
            }

            return archiveFile;
        }

Будьте осторожны, чтобы обеспечить расширение файла внутри имени файла для создания записи в методе архивирования.