Files
duplicati/Duplicati/Library/Backend/Box/BoxBackend.cs
T
2018-10-15 21:20:30 -07:00

500 lines
16 KiB
C#

// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// This library is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation; either version 2.1 of the
// License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Linq;
using Duplicati.Library.Interface;
using System.Collections.Generic;
using Newtonsoft.Json;
using System.Net;
namespace Duplicati.Library.Backend.Box
{
public class BoxBackend : IBackend, IStreamingBackend
{
private static readonly string LOGTAG = Logging.Log.LogTagFromType<BoxBackend>();
private const string AUTHID_OPTION = "authid";
private const string REALLY_DELETE_OPTION = "box-delete-from-trash";
private const string BOX_API_URL = "https://api.box.com/2.0";
private const string BOX_UPLOAD_URL = "https://upload.box.com/api/2.0/files";
private const int PAGE_SIZE = 200;
private readonly BoxHelper m_oauth;
private readonly string m_path;
private readonly bool m_deleteFromTrash;
private string m_currentfolder;
private readonly Dictionary<string, string> m_filecache = new Dictionary<string, string>();
private class BoxHelper : OAuthHelper
{
public BoxHelper(string authid)
: base(authid, "box.com")
{
AutoAuthHeader = true;
}
protected override void ParseException(Exception ex)
{
Exception newex = null;
try
{
if (ex is WebException && (ex as WebException).Response is HttpWebResponse)
{
string rawdata = null;
var hs = (ex as WebException).Response as HttpWebResponse;
using(var rs = Library.Utility.AsyncHttpRequest.TrySetTimeout(hs.GetResponseStream()))
using(var sr = new System.IO.StreamReader(rs))
rawdata = sr.ReadToEnd();
if (string.IsNullOrWhiteSpace(rawdata))
return;
newex = new Exception("Raw message: " + rawdata);
var msg = JsonConvert.DeserializeObject<ErrorResponse>(rawdata);
newex = new Exception(string.Format("{0} - {1}: {2}", msg.Status, msg.Code, msg.Message));
/*if (msg.ContextInfo != null && msg.ContextInfo.Length > 0)
newex = new Exception(string.Format("{0} - {1}: {2}{3}{4}", msg.Status, msg.Code, msg.Message, Environment.NewLine, string.Join("; ", from n in msg.ContextInfo select n.Message)));
*/
}
}
catch(Exception ex2)
{
Library.Logging.Log.WriteWarningMessage(LOGTAG, "BoxErrorParser", ex2, "Failed to parse error from Box");
}
if (newex != null)
throw newex;
}
}
public BoxBackend()
{
}
public BoxBackend(string url, Dictionary<string, string> options)
{
var uri = new Utility.Uri(url);
m_path = Duplicati.Library.Utility.Utility.AppendDirSeparator(uri.HostAndPath, "/");
string authid = null;
if (options.ContainsKey(AUTHID_OPTION))
authid = options[AUTHID_OPTION];
m_deleteFromTrash = Library.Utility.Utility.ParseBoolOption(options, REALLY_DELETE_OPTION);
m_oauth = new BoxHelper(authid);
}
private string CurrentFolder
{
get
{
if (m_currentfolder == null)
GetCurrentFolder(false);
return m_currentfolder;
}
}
private void GetCurrentFolder(bool create)
{
var parentid = "0";
foreach(var p in m_path.Split(new string[] {"/"}, StringSplitOptions.RemoveEmptyEntries))
{
var el = (MiniFolder)PagedFileListResponse(parentid, true).FirstOrDefault(x => x.Name == p);
if (el == null)
{
if (!create)
throw new FolderMissingException();
el = m_oauth.PostAndGetJSONData<ListFolderResponse>(
string.Format("{0}/folders", BOX_API_URL),
new CreateItemRequest() { Name = p, Parent = new IDReference() { ID = parentid } }
);
}
parentid = el.ID;
}
m_currentfolder = parentid;
}
private string GetFileID(string name)
{
if (m_filecache.ContainsKey(name))
return m_filecache[name];
// Make sure we enumerate this, otherwise the m_filecache is empty.
PagedFileListResponse(CurrentFolder, false).LastOrDefault();
if (m_filecache.ContainsKey(name))
return m_filecache[name];
throw new FileMissingException();
}
private IEnumerable<FileEntity> PagedFileListResponse(string parentid, bool onlyfolders)
{
var offset = 0;
var done = false;
if (!onlyfolders)
m_filecache.Clear();
do
{
var resp = m_oauth.GetJSONData<ShortListResponse>(string.Format("{0}/folders/{1}/items?limit={2}&offset={3}&fields=name,size,modified_at", BOX_API_URL, parentid, PAGE_SIZE, offset));
if (resp.Entries == null || resp.Entries.Length == 0)
break;
foreach(var f in resp.Entries)
{
if (onlyfolders && f.Type != "folder")
{
done = true;
break;
}
else
{
if (!onlyfolders && f.Type == "file")
m_filecache[f.Name] = f.ID;
yield return f;
}
}
offset = offset + PAGE_SIZE;
if (offset >= resp.TotalCount)
break;
} while(!done);
}
#region IStreamingBackend implementation
public void Put(string remotename, System.IO.Stream stream)
{
var createreq = new CreateItemRequest() {
Name = remotename,
Parent = new IDReference() {
ID = CurrentFolder
}
};
if (m_filecache.Count == 0)
PagedFileListResponse(CurrentFolder, false);
var existing = m_filecache.ContainsKey(remotename);
try
{
FileEntity res;
if (existing)
{
res = m_oauth.PostMultipartAndGetJSONData<FileList>(
string.Format("{0}/{1}/content", BOX_UPLOAD_URL, m_filecache[remotename]),
new MultipartItem(stream, "file", remotename)
).Entries.First();
}
else
{
res = m_oauth.PostMultipartAndGetJSONData<FileList>(
string.Format("{0}/content", BOX_UPLOAD_URL),
new MultipartItem(createreq, "attributes"),
new MultipartItem(stream, "file", remotename)
).Entries.First();
}
m_filecache[remotename] = res.ID;
}
catch
{
m_filecache.Clear();
throw;
}
}
public void Get(string remotename, System.IO.Stream stream)
{
using (var resp = m_oauth.GetResponse(string.Format("{0}/files/{1}/content", BOX_API_URL, GetFileID(remotename))))
using(var rs = Duplicati.Library.Utility.AsyncHttpRequest.TrySetTimeout(resp.GetResponseStream()))
Library.Utility.Utility.CopyStream(rs, stream);
}
#endregion
#region IBackend implementation
public System.Collections.Generic.IEnumerable<IFileEntry> List()
{
return
from n in PagedFileListResponse(CurrentFolder, false)
select (IFileEntry)new FileEntry(n.Name, n.Size, n.ModifiedAt, n.ModifiedAt) { IsFolder = n.Type == "folder" };
}
public void Put(string remotename, string filename)
{
using (System.IO.FileStream fs = System.IO.File.OpenRead(filename))
Put(remotename, fs);
}
public void Get(string remotename, string filename)
{
using (System.IO.FileStream fs = System.IO.File.Create(filename))
Get(remotename, fs);
}
public void Delete(string remotename)
{
var fileid = GetFileID(remotename);
try
{
using(var r = m_oauth.GetResponse(string.Format("{0}/files/{1}", BOX_API_URL, fileid), null, "DELETE"))
{
}
if (m_deleteFromTrash)
using(var r = m_oauth.GetResponse(string.Format("{0}/files/{1}/trash", BOX_API_URL, fileid), null, "DELETE"))
{
}
}
catch
{
m_filecache.Clear();
throw;
}
}
public void Test()
{
this.TestList();
}
public void CreateFolder()
{
GetCurrentFolder(true);
}
public string DisplayName
{
get
{
return Strings.Box.DisplayName;
}
}
public string ProtocolKey
{
get
{
return "box";
}
}
public IList<ICommandLineArgument> SupportedCommands
{
get {
return new List<ICommandLineArgument>(new ICommandLineArgument[] {
new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Box.AuthidShort, Strings.Box.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("box.com"))),
new CommandLineArgument(REALLY_DELETE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Box.ReallydeleteShort, Strings.Box.ReallydeleteLong),
});
}
}
public string Description
{
get
{
return Strings.Box.Description;
}
}
public string[] DNSName
{
get { return new string[] { new Uri(BOX_API_URL).Host, new Uri(BOX_UPLOAD_URL).Host }; }
}
#endregion
#region IDisposable implementation
public void Dispose()
{
}
#endregion
private class MiniUser : IDReference
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("login")]
public string Login { get; set; }
}
private class MiniFolder : IDReference
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("etag")]
public string ETag { get; set; }
[JsonProperty("sequence_id")]
public string SequenceID { get; set; }
}
private class FileEntity : MiniFolder
{
public FileEntity() { Size = -1; }
[JsonProperty("sha1")]
public string SHA1 { get; set; }
[JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)]
public long Size { get; set; }
[JsonProperty("modified_at", NullValueHandling = NullValueHandling.Ignore)]
public DateTime ModifiedAt { get; set; }
}
private class FolderList
{
[JsonProperty("total_count")]
public long TotalCount { get; set; }
[JsonProperty("entries")]
public MiniFolder[] Entries { get; set; }
}
private class FileList
{
[JsonProperty("total_count")]
public long TotalCount { get; set; }
[JsonProperty("entries")]
public FileEntity[] Entries { get; set; }
[JsonProperty("offset")]
public long Offset { get; set; }
[JsonProperty("limit")]
public long Limit { get; set; }
}
private class UploadEmail
{
[JsonProperty("access")]
public string Access { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
}
private class ListFolderResponse : MiniFolder
{
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
[JsonProperty("modified_at")]
public DateTime ModifiedAt { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("size")]
public long Size { get; set; }
[JsonProperty("path_collection")]
public FolderList PathCollection { get; set; }
[JsonProperty("created_by")]
public MiniUser CreatedBy { get; set; }
[JsonProperty("modified_by")]
public MiniUser ModifiedBy { get; set; }
[JsonProperty("owned_by")]
public MiniUser OwnedBy { get; set; }
[JsonProperty("shared_link")]
public MiniUser SharedLink { get; set; }
[JsonProperty("folder_upload_email")]
public UploadEmail FolderUploadEmail { get; set; }
[JsonProperty("parent")]
public MiniFolder Parent { get; set; }
[JsonProperty("item_status")]
public string ItemStatus { get; set; }
[JsonProperty("item_collection")]
public FileList ItemCollection { get; set; }
}
private class OrderEntry
{
[JsonProperty("by")]
public string By { get; set; }
[JsonProperty("direction")]
public string Direction { get; set; }
}
private class ShortListResponse : FileList
{
[JsonProperty("order")]
public OrderEntry[] Order { get; set; }
}
private class IDReference
{
[JsonProperty("id")]
public string ID { get; set; }
}
private class CreateItemRequest
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("parent")]
public IDReference Parent { get; set; }
}
private class ErrorResponse
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("status")]
public int Status { get; set; }
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("help_url")]
public string HelpUrl { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("request_id")]
public string RequestId { get; set; }
}
}
}