2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
2018-11-02 21:34:07 +01:00
using Duplicati.Library.Common.IO ;
2019-02-22 21:58:40 -06:00
using Duplicati.Library.Interface ;
2015-12-07 09:57:22 +01:00
using Newtonsoft.Json ;
2019-02-22 21:58:40 -06:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2015-12-07 09:57:22 +01:00
using System.Net ;
2019-02-22 21:58:40 -06:00
using System.Threading ;
using System.Threading.Tasks ;
2015-12-07 09:57:22 +01:00
namespace Duplicati.Library.Backend.Box
{
2019-04-28 13:34:15 -07:00
// ReSharper disable once ClassNeverInstantiated.Global
// This class is instantiated dynamically in the BackendLoader.
2015-12-07 09:57:22 +01:00
public class BoxBackend : IBackend , IStreamingBackend
{
2018-05-15 11:29:08 +02:00
private static readonly string LOGTAG = Logging . Log . LogTagFromType < BoxBackend >();
2015-12-07 09:57:22 +01:00
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 ;
2018-05-23 21:18:01 -07:00
private readonly BoxHelper m_oauth ;
private readonly string m_path ;
private readonly bool m_deleteFromTrash ;
2015-12-07 09:57:22 +01:00
private string m_currentfolder ;
2018-06-16 11:02:18 -07:00
private readonly Dictionary < string , string > m_filecache = new Dictionary < string , string >();
2015-12-07 09:57:22 +01:00
private class BoxHelper : OAuthHelper
{
public BoxHelper ( string authid )
: base ( authid , "box.com" )
{
AutoAuthHeader = true ;
}
protected override void ParseException ( Exception ex )
{
Exception newex = null ;
try
{
2019-09-29 20:16:28 -07:00
if ( ex is WebException exception && exception . Response is HttpWebResponse hs )
2015-12-07 09:57:22 +01:00
{
string rawdata = null ;
2016-09-15 11:39:27 +02:00
using ( var rs = Library . Utility . AsyncHttpRequest . TrySetTimeout ( hs . GetResponseStream ()))
2015-12-07 09:57:22 +01:00
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 )
{
2018-05-15 11:29:08 +02:00
Library . Logging . Log . WriteWarningMessage ( LOGTAG , "BoxErrorParser" , ex2 , "Failed to parse error from Box" );
2015-12-07 09:57:22 +01:00
}
if ( newex != null )
throw newex ;
}
}
2019-04-28 13:35:07 -07:00
// ReSharper disable once UnusedMember.Global
// This constructor is needed by the BackendLoader.
2015-12-07 09:57:22 +01:00
public BoxBackend ()
{
}
2019-04-28 13:35:07 -07:00
// ReSharper disable once UnusedMember.Global
// This constructor is needed by the BackendLoader.
2015-12-07 09:57:22 +01:00
public BoxBackend ( string url , Dictionary < string , string > options )
{
var uri = new Utility . Uri ( url );
2018-10-27 12:17:07 +02:00
m_path = Util . AppendDirSeparator ( uri . HostAndPath , "/" );
2018-09-21 16:26:30 -07:00
2015-12-07 09:57:22 +01:00
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 ))
{
2018-10-06 15:50:05 -07:00
var el = ( MiniFolder ) PagedFileListResponse ( parentid , true ). FirstOrDefault ( x => x . Name == p );
2015-12-07 09:57:22 +01:00
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 )
{
2018-06-16 11:02:18 -07:00
if ( m_filecache . ContainsKey ( name ))
2015-12-07 09:57:22 +01:00
return m_filecache [ name ];
2018-06-16 11:02:18 -07:00
// Make sure we enumerate this, otherwise the m_filecache is empty.
2018-02-08 21:45:36 +01:00
PagedFileListResponse ( CurrentFolder , false ). LastOrDefault ();
2015-12-07 09:57:22 +01:00
2018-06-16 11:02:18 -07:00
if ( m_filecache . ContainsKey ( name ))
2015-12-07 09:57:22 +01:00
return m_filecache [ name ];
throw new FileMissingException ();
}
private IEnumerable < FileEntity > PagedFileListResponse ( string parentid , bool onlyfolders )
{
var offset = 0 ;
var done = false ;
if (! onlyfolders )
2018-06-16 11:02:18 -07:00
m_filecache . Clear ();
2015-12-07 09:57:22 +01:00
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" )
2018-06-16 11:02:18 -07:00
m_filecache [ f . Name ] = f . ID ;
2015-12-07 09:57:22 +01:00
yield return f ;
}
}
offset = offset + PAGE_SIZE ;
if ( offset >= resp . TotalCount )
break ;
} while (! done );
}
#region IStreamingBackend implementation
2019-03-17 18:20:14 -05:00
public async Task PutAsync ( string remotename , System . IO . Stream stream , CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
var createreq = new CreateItemRequest () {
Name = remotename ,
Parent = new IDReference () {
ID = CurrentFolder
}
};
2018-06-16 11:02:18 -07:00
if ( m_filecache . Count == 0 )
2015-12-07 09:57:22 +01:00
PagedFileListResponse ( CurrentFolder , false );
var existing = m_filecache . ContainsKey ( remotename );
try
{
2019-02-23 09:13:40 -06:00
string url ;
var items = new List < MultipartItem >( 2 );
2015-12-07 09:57:22 +01:00
if ( existing )
2019-02-23 09:13:40 -06:00
url = $"{BOX_UPLOAD_URL}/{m_filecache[remotename]}/content" ;
2015-12-07 09:57:22 +01:00
else
{
2019-02-23 09:13:40 -06:00
url = $"{BOX_UPLOAD_URL}/content" ;
items . Add ( new MultipartItem ( createreq , "attributes" ));
2015-12-07 09:57:22 +01:00
}
2019-02-23 09:13:40 -06:00
items . Add ( new MultipartItem ( stream , "file" , remotename ));
var res = ( await m_oauth . PostMultipartAndGetJSONDataAsync < FileList >( url , null , cancelToken , items . ToArray ())). Entries . First ();
2015-12-07 09:57:22 +01:00
m_filecache [ remotename ] = res . ID ;
}
catch
{
2018-06-16 11:02:18 -07:00
m_filecache . Clear ();
2015-12-07 09:57:22 +01:00
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 ))))
2016-09-15 11:39:27 +02:00
using ( var rs = Duplicati . Library . Utility . AsyncHttpRequest . TrySetTimeout ( resp . GetResponseStream ()))
2015-12-07 09:57:22 +01:00
Library . Utility . Utility . CopyStream ( rs , stream );
}
#endregion
#region IBackend implementation
2017-09-25 23:17:45 -06:00
public System . Collections . Generic . IEnumerable < IFileEntry > List ()
2015-12-07 09:57:22 +01:00
{
2017-09-25 23:17:45 -06:00
return
2015-12-07 09:57:22 +01:00
from n in PagedFileListResponse ( CurrentFolder , false )
2017-09-25 23:17:45 -06:00
select ( IFileEntry ) new FileEntry ( n . Name , n . Size , n . ModifiedAt , n . ModifiedAt ) { IsFolder = n . Type == "folder" };
2015-12-07 09:57:22 +01:00
}
2021-06-12 11:30:08 -07:00
public async Task PutAsync ( string remotename , string filename , CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
using ( System . IO . FileStream fs = System . IO . File . OpenRead ( filename ))
2021-06-12 11:30:08 -07:00
await PutAsync ( remotename , fs , cancelToken );
2015-12-07 09:57:22 +01:00
}
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
{
2018-06-16 11:02:18 -07:00
m_filecache . Clear ();
2015-12-07 09:57:22 +01:00
throw ;
}
}
public void Test ()
{
2017-09-25 23:17:45 -06:00
this . TestList ();
2015-12-07 09:57:22 +01:00
}
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 ;
}
}
2018-02-26 11:37:10 +01:00
public string [] DNSName
2018-02-21 22:38:01 +01:00
{
2018-02-26 11:37:10 +01:00
get { return new string [] { new Uri ( BOX_API_URL ). Host , new Uri ( BOX_UPLOAD_URL ). Host }; }
2018-02-21 22:38:01 +01:00
}
2018-02-18 00:18:44 +01:00
2015-12-07 09:57:22 +01:00
#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 ; }
}
}
}