Google Drive partial support (#19)
* Initial Google Drive support * Persistent GitHub auth state * Fix uwp compilation * Fix xf compilation * Download and upload functionality
This commit is contained in:
Родитель
9a38ed0997
Коммит
b6a397280b
|
@ -58,7 +58,8 @@ namespace Camelotia.Presentation.Avalonia
|
|||
["Yandex Disk"] = id => new YandexFileSystemProvider(id, login, cache),
|
||||
["FTP"] = id => new FtpFileSystemProvider(id),
|
||||
["SFTP"] = id => new SftpFileSystemProvider(id),
|
||||
["GitHub"] = id => new GitHubFileSystemProvider(id)
|
||||
["GitHub"] = id => new GitHubFileSystemProvider(id, cache),
|
||||
["Google Drive"] = id => new GoogleDriveFileSystemProvider(id, cache)
|
||||
},
|
||||
cache
|
||||
),
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
SelectionMode="Toggle"
|
||||
Items="{Binding Files}"
|
||||
IsVisible="{Binding IsReady}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
SelectedItem="{Binding SelectedFile, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="models:FileModel">
|
||||
|
|
|
@ -46,7 +46,7 @@ namespace Camelotia.Presentation.Uwp
|
|||
["Vkontakte Docs"] = id => new VkontakteFileSystemProvider(id, cache),
|
||||
["FTP"] = id => new FtpFileSystemProvider(id),
|
||||
["SFTP"] = id => new SftpFileSystemProvider(id),
|
||||
["GitHub"] = id => new GitHubFileSystemProvider(id)
|
||||
["GitHub"] = id => new GitHubFileSystemProvider(id, cache)
|
||||
},
|
||||
cache
|
||||
),
|
||||
|
|
|
@ -80,7 +80,7 @@ namespace Camelotia.Presentation.Xamarin.Droid
|
|||
["Yandex Disk"] = id => new YandexFileSystemProvider(id, login, cache),
|
||||
["FTP"] = id => new FtpFileSystemProvider(id),
|
||||
["SFTP"] = id => new SftpFileSystemProvider(id),
|
||||
["GitHub"] = id => new GitHubFileSystemProvider(id)
|
||||
["GitHub"] = id => new GitHubFileSystemProvider(id, cache)
|
||||
},
|
||||
cache
|
||||
),
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<PackageReference Include="DynamicData" Version="6.7.1.2534" />
|
||||
<PackageReference Include="akavache" Version="6.2.3" />
|
||||
<PackageReference Include="FluentFTP" Version="19.2.2" />
|
||||
<PackageReference Include="Google.Apis.Drive.v3" Version="1.38.0.1530" />
|
||||
<PackageReference Include="Octokit" Version="0.32.0" />
|
||||
<PackageReference Include="ssh.net" Version="2016.1.0" />
|
||||
<PackageReference Include="System.Reactive" Version="4.1.2" />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Camelotia.Services.Models
|
||||
{
|
||||
|
@ -18,20 +19,19 @@ namespace Camelotia.Services.Models
|
|||
|
||||
public FileModel(string name, string path, bool isFolder, string size, DateTime? modified = null)
|
||||
{
|
||||
Name = name;
|
||||
Path = path;
|
||||
Size = size;
|
||||
IsFolder = isFolder;
|
||||
Modified = modified?.ToString();
|
||||
Name = new string(name.Take(40).ToArray());
|
||||
}
|
||||
|
||||
public override int GetHashCode() => (Name, Path, IsFolder, Size).GetHashCode();
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var file = obj as FileModel;
|
||||
return
|
||||
file != null &&
|
||||
obj is FileModel file &&
|
||||
file.Name == Name &&
|
||||
file.Path == Path &&
|
||||
file.IsFolder == IsFolder &&
|
||||
|
|
|
@ -9,5 +9,7 @@ namespace Camelotia.Services.Models
|
|||
public string Type { get; set; }
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
public string User { get; set; }
|
||||
}
|
||||
}
|
|
@ -3,9 +3,11 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using Akavache;
|
||||
using Camelotia.Services.Interfaces;
|
||||
using Camelotia.Services.Models;
|
||||
using Octokit;
|
||||
|
@ -18,23 +20,26 @@ namespace Camelotia.Services.Providers
|
|||
private readonly GitHubClient _gitHub = new GitHubClient(new ProductHeaderValue(GithubApplicationId));
|
||||
private readonly ISubject<bool> _isAuthenticated = new ReplaySubject<bool>(1);
|
||||
private readonly HttpClient _httpClient = new HttpClient();
|
||||
private readonly IBlobCache _blobCache;
|
||||
private string _currentUserName;
|
||||
|
||||
public GitHubFileSystemProvider(Guid id)
|
||||
public GitHubFileSystemProvider(Guid id, IBlobCache blobCache)
|
||||
{
|
||||
Id = id;
|
||||
_blobCache = blobCache;
|
||||
_isAuthenticated.OnNext(false);
|
||||
EnsureLoggedInIfTokenSaved();
|
||||
}
|
||||
|
||||
public Guid Id { get; }
|
||||
|
||||
public string Size => "Unknown";
|
||||
public string Size { get; } = "Unknown";
|
||||
|
||||
public string Name => "GitHub";
|
||||
public string Name { get; } = "GitHub";
|
||||
|
||||
public string Description => "GitHub repositories provider.";
|
||||
public string Description { get; } = "GitHub repositories provider.";
|
||||
|
||||
public string InitialPath => string.Empty;
|
||||
public string InitialPath { get; } = string.Empty;
|
||||
|
||||
public IObservable<bool> IsAuthorized => _isAuthenticated;
|
||||
|
||||
|
@ -55,6 +60,13 @@ namespace Camelotia.Services.Providers
|
|||
_currentUserName = login;
|
||||
_gitHub.Credentials = new Credentials(login, password);
|
||||
await _gitHub.User.Current();
|
||||
|
||||
var persistentId = Id.ToString();
|
||||
var model = await _blobCache.GetObject<ProviderModel>(persistentId);
|
||||
model.Token = password;
|
||||
model.User = login;
|
||||
|
||||
await _blobCache.InsertObject(persistentId, model);
|
||||
_isAuthenticated.OnNext(true);
|
||||
}
|
||||
|
||||
|
@ -118,20 +130,23 @@ namespace Camelotia.Services.Providers
|
|||
to.Close();
|
||||
}
|
||||
|
||||
public Task CreateFolder(string path, string name)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task CreateFolder(string path, string name) => throw new NotImplementedException();
|
||||
|
||||
public Task RenameFile(FileModel file, string name)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public Task RenameFile(FileModel file, string name) => throw new NotImplementedException();
|
||||
|
||||
public Task UploadFile(string to, Stream from, string name) => throw new NotImplementedException();
|
||||
|
||||
public Task Delete(FileModel file) => throw new NotImplementedException();
|
||||
|
||||
private async void EnsureLoggedInIfTokenSaved()
|
||||
{
|
||||
var persistentId = Id.ToString();
|
||||
var model = await _blobCache.GetOrFetchObject(persistentId, () => Task.FromResult(default(ProviderModel)));
|
||||
if (model?.User == null || model?.Token == null) return;
|
||||
_gitHub.Credentials = new Credentials(model.User, model.Token);
|
||||
_isAuthenticated.OnNext(true);
|
||||
}
|
||||
|
||||
private static (string Repository, string Path, string Separator) GetRepositoryNameAndFilePath(string input)
|
||||
{
|
||||
var separator = Path.DirectorySeparatorChar;
|
||||
|
@ -145,15 +160,5 @@ namespace Camelotia.Services.Providers
|
|||
var path = Path.Combine(pathParts);
|
||||
return (repositoryName, path, separator.ToString());
|
||||
}
|
||||
|
||||
private static string GetSha1Hash(Stream stream)
|
||||
{
|
||||
using (var sha1 = new SHA1CryptoServiceProvider())
|
||||
{
|
||||
var hash = sha1.ComputeHash(stream);
|
||||
var hashStr = Convert.ToBase64String(hash);
|
||||
return hashStr.TrimEnd('=');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Akavache;
|
||||
using Camelotia.Services.Interfaces;
|
||||
using Camelotia.Services.Models;
|
||||
using Google.Apis.Auth.OAuth2;
|
||||
using Google.Apis.Download;
|
||||
using Google.Apis.Drive.v3;
|
||||
using Google.Apis.Drive.v3.Data;
|
||||
using Google.Apis.Services;
|
||||
using Google.Apis.Util.Store;
|
||||
using File = Google.Apis.Drive.v3.Data.File;
|
||||
|
||||
namespace Camelotia.Services.Providers
|
||||
{
|
||||
public sealed class GoogleDriveFileSystemProvider : IProvider
|
||||
{
|
||||
private const string GoogleDriveApplicationName = "Camelotia";
|
||||
private const string GoogleDriveClientId = "1096201018044-qbv35mo5cd7b5utfjpg83v5lsuhssvvg.apps.googleusercontent.com";
|
||||
private const string GoogleDriveClientSecret = "w4F099v9awUEAs66rmCxLbYr";
|
||||
private const string GoogleDriveUserName = "user";
|
||||
|
||||
private readonly ISubject<bool> _isAuthorized = new ReplaySubject<bool>(1);
|
||||
private readonly IBlobCache _blobCache;
|
||||
private DriveService _driveService;
|
||||
|
||||
public GoogleDriveFileSystemProvider(Guid id, IBlobCache blobCache)
|
||||
{
|
||||
Id = id;
|
||||
_blobCache = blobCache;
|
||||
_isAuthorized.OnNext(false);
|
||||
EnsureLoggedInIfTokenSaved();
|
||||
}
|
||||
|
||||
public Guid Id { get; }
|
||||
|
||||
public string Size { get; } = "Unknown";
|
||||
|
||||
public string Name { get; } = "Google Drive";
|
||||
|
||||
public string Description { get; } = "Google Drive file system.";
|
||||
|
||||
public string InitialPath { get; } = "/";
|
||||
|
||||
public IObservable<bool> IsAuthorized => _isAuthorized;
|
||||
|
||||
public bool SupportsDirectAuth => false;
|
||||
|
||||
public bool SupportsHostAuth => false;
|
||||
|
||||
public bool SupportsOAuth => true;
|
||||
|
||||
public bool CanCreateFolder => false;
|
||||
|
||||
public async Task<IEnumerable<FileModel>> Get(string path)
|
||||
{
|
||||
var list = _driveService.Files.List();
|
||||
list.PageSize = 1000;
|
||||
list.Fields = "files(id, name, size, modifiedTime)";
|
||||
|
||||
var response = await list.ExecuteAsync().ConfigureAwait(false);
|
||||
var files = from file in response.Files
|
||||
let size = file.Size.GetValueOrDefault()
|
||||
let bytes = ByteConverter.BytesToString(size)
|
||||
select new FileModel(file.Name, file.Id, false, bytes, file.ModifiedTime);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public async Task UploadFile(string to, Stream from, string name)
|
||||
{
|
||||
var create = _driveService.Files.Create(new File {Name = name}, from, "application/vnd.google-apps.file");
|
||||
await create.UploadAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DownloadFile(string from, Stream to)
|
||||
{
|
||||
var file = _driveService.Files.Get(from);
|
||||
var progress = await file.DownloadAsync(to).ConfigureAwait(false);
|
||||
while (progress.Status == DownloadStatus.Downloading)
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
public async Task RenameFile(FileModel file, string name)
|
||||
{
|
||||
var update = _driveService.Files.Update(new File {Name = name}, file.Path);
|
||||
await update.ExecuteAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task Delete(FileModel file)
|
||||
{
|
||||
var delete = _driveService.Files.Delete(file.Path);
|
||||
await delete.ExecuteAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task CreateFolder(string path, string name) => Task.CompletedTask;
|
||||
|
||||
public Task HostAuth(string address, int port, string login, string password) => Task.CompletedTask;
|
||||
|
||||
public Task DirectAuth(string login, string password) => Task.CompletedTask;
|
||||
|
||||
public Task OAuth() => Task.Run(AuthenticateAsync);
|
||||
|
||||
public Task Logout() => Task.Run(async () =>
|
||||
{
|
||||
var keys = await _blobCache.GetAllKeys();
|
||||
var googleDriveKeys = keys.Where(x => x.StartsWith("google-drive"));
|
||||
foreach (var driveKey in googleDriveKeys)
|
||||
await _blobCache.Invalidate(driveKey);
|
||||
|
||||
_driveService = null;
|
||||
_isAuthorized.OnNext(false);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
private void EnsureLoggedInIfTokenSaved() => Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await AuthenticateAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
private async Task AuthenticateAsync()
|
||||
{
|
||||
var credential = await GoogleWebAuthorizationBroker
|
||||
.AuthorizeAsync(
|
||||
new ClientSecrets {ClientId = GoogleDriveClientId, ClientSecret = GoogleDriveClientSecret},
|
||||
new[] {DriveService.Scope.Drive},
|
||||
GoogleDriveUserName,
|
||||
CancellationToken.None,
|
||||
new AkavacheDataStore(_blobCache))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var initializer = new BaseClientService.Initializer
|
||||
{
|
||||
HttpClientInitializer = credential,
|
||||
ApplicationName = GoogleDriveApplicationName
|
||||
};
|
||||
|
||||
_driveService = new DriveService(initializer);
|
||||
_isAuthorized.OnNext(true);
|
||||
}
|
||||
|
||||
private sealed class AkavacheDataStore : IDataStore
|
||||
{
|
||||
private readonly IBlobCache _blobCache;
|
||||
|
||||
public AkavacheDataStore(IBlobCache blobCache) => _blobCache = blobCache;
|
||||
|
||||
public async Task StoreAsync<T>(string key, T value)
|
||||
{
|
||||
var identity = $"google-drive-{key}";
|
||||
await _blobCache.InsertObject(identity, value);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync<T>(string key)
|
||||
{
|
||||
var identity = $"google-drive-{key}";
|
||||
await _blobCache.Invalidate(identity);
|
||||
}
|
||||
|
||||
public async Task<T> GetAsync<T>(string key)
|
||||
{
|
||||
var identity = $"google-drive-{key}";
|
||||
var value = await _blobCache.GetOrFetchObject<T>(identity, () => Task.FromResult(default(T)));
|
||||
return value;
|
||||
}
|
||||
|
||||
public Task ClearAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -179,7 +179,7 @@ namespace Camelotia.Services.Providers
|
|||
private async void EnsureLoggedInIfTokenSaved()
|
||||
{
|
||||
var persistentId = Id.ToString();
|
||||
var model = await _blobCache.GetOrFetchObject(persistentId, () => Observable.Return<ProviderModel>(null));
|
||||
var model = await _blobCache.GetOrFetchObject(persistentId, () => Task.FromResult(default(ProviderModel)));
|
||||
var token = model?.Token;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token)) return;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[![Build Status](https://worldbeater.visualstudio.com/Camelotia/_apis/build/status/Camelotia-CI)](https://worldbeater.visualstudio.com/Camelotia/_build/latest?definitionId=1) [![Pull Requests](https://img.shields.io/github/issues-pr/worldbeater/camelotia.svg)](https://github.com/worldbeater/Camelotia/pulls) [![Issues](https://img.shields.io/github/issues/worldbeater/camelotia.svg)](https://github.com/worldbeater/Camelotia/issues) ![License](https://img.shields.io/github/license/worldbeater/camelotia.svg) ![Size](https://img.shields.io/github/repo-size/worldbeater/camelotia.svg)
|
||||
|
||||
The app runs on Windows, Linux, MacOS, XBox, Surface Hub and HoloLens. Built with [ReactiveUI](https://github.com/reactiveui/ReactiveUI).
|
||||
File manager for cloud storages. Supports Yandex Disk, Google Drive, VK Documents, GitHub, FTP, SFTP. The app runs on Windows, Linux, MacOS, XBox, Surface Hub and HoloLens. Built with [ReactiveUI](https://github.com/reactiveui/ReactiveUI).
|
||||
|
||||
## Compiling Avalonia app
|
||||
|
||||
|
@ -25,8 +25,6 @@ You can compile Universal Windows Platform Camelotia app only on latest Windows
|
|||
|
||||
Supports light and dark themes!
|
||||
|
||||
<img src="./UiWindows2.jpg" width="550">
|
||||
|
||||
## Compiling Xamarin Forms app
|
||||
|
||||
To compile the Xamarin Forms Android application, you need to install appropriate Android SDK v8.1. This can be achieved by using [Visual Studio Installer](https://visualstudio.microsoft.com/ru/vs/) and selecting "Mobile Development" section there.
|
||||
|
@ -51,4 +49,5 @@ File system providers are located at `./Camelotia.Services/Providers/`. To add a
|
|||
- <a href="https://github.com/robinrodricks/FluentFTP">FluentFTP</a> FTP implementation
|
||||
- <a href="https://github.com/sshnet/SSH.NET/">SSH.NET</a> SFTP implementation
|
||||
- <a href="https://github.com/vknet/vk">VkNet</a> Vkontakte SDK for .NET
|
||||
- <a href="https://github.com/googleapis/google-api-dotnet-client">Google Drive</a> SDK for .NET
|
||||
- <a href="https://www.jetbrains.com/rider/">JetBrains Rider</a> and <a href="https://visualstudio.microsoft.com/">Microsoft Visual Studio</a> IDEs
|
||||
|
|
Двоичные данные
UiWindows2.jpg
Двоичные данные
UiWindows2.jpg
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 138 KiB |
Загрузка…
Ссылка в новой задаче