2025-02-17 16:45:51 +01:00
// Copyright (C) 2025, 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
2024-02-28 15:45:30 +01:00
// 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 ;
2025-02-17 16:45:51 +01:00
using System.Runtime.CompilerServices ;
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
{
2024-09-29 22:00:57 +02:00
private static readonly string LOGTAG = Logging . Log . LogTagFromType < BoxBackend >();
2018-05-15 11:29:08 +02:00
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 ;
2024-09-29 22:00:57 +02:00
using ( var rs = Library . Utility . AsyncHttpRequest . TrySetTimeout ( hs . GetResponseStream ()))
using ( var sr = new System . IO . StreamReader ( rs ))
2015-12-07 09:57:22 +01:00
rawdata = sr . ReadToEnd ();
if ( string . IsNullOrWhiteSpace ( rawdata ))
return ;
2024-09-29 22:00:57 +02:00
2015-12-07 09:57:22 +01:00
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)));
*/
}
}
2024-09-29 22:00:57 +02:00
catch ( Exception ex2 )
2015-12-07 09:57:22 +01:00
{
2024-09-29 22:00:57 +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 )
2024-09-29 22:00:57 +02:00
throw newex ;
2015-12-07 09:57:22 +01:00
}
}
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 );
}
2024-10-01 09:30:02 +02:00
private async Task < string > GetCurrentFolderWithCacheAsync ( CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
2024-10-01 09:30:02 +02:00
if ( m_currentfolder == null )
await GetCurrentFolderAsync ( false , cancelToken ). ConfigureAwait ( false );
2024-09-29 22:00:57 +02:00
2024-10-01 09:30:02 +02:00
return m_currentfolder ;
2015-12-07 09:57:22 +01:00
}
2024-10-01 09:30:02 +02:00
private async Task GetCurrentFolderAsync ( bool create , CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
var parentid = "0" ;
2024-09-29 22:00:57 +02:00
foreach ( var p in m_path . Split ( new string [] { "/" }, StringSplitOptions . RemoveEmptyEntries ))
2015-12-07 09:57:22 +01:00
{
2025-02-17 16:45:51 +01:00
var el = ( MiniFolder ) await PagedFileListResponse ( parentid , true , cancelToken ). FirstOrDefaultAsync ( x => x . Name == p ). ConfigureAwait ( false );
2015-12-07 09:57:22 +01:00
if ( el == null )
{
if (! create )
throw new FolderMissingException ();
2024-10-01 09:30:02 +02:00
el = await m_oauth . PostAndGetJSONDataAsync < ListFolderResponse >(
2015-12-07 09:57:22 +01:00
string . Format ( "{0}/folders" , BOX_API_URL ),
2024-10-01 09:30:02 +02:00
cancelToken ,
2015-12-07 09:57:22 +01:00
new CreateItemRequest () { Name = p , Parent = new IDReference () { ID = parentid } }
2024-10-01 09:30:02 +02:00
). ConfigureAwait ( false );
2015-12-07 09:57:22 +01:00
}
parentid = el . ID ;
}
m_currentfolder = parentid ;
}
2024-10-01 09:30:02 +02:00
private async Task < string > GetFileIDAsync ( string name , CancellationToken cancelToken )
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 ];
2018-06-16 11:02:18 -07:00
// Make sure we enumerate this, otherwise the m_filecache is empty.
2024-10-01 09:30:02 +02:00
var currentFolder = await GetCurrentFolderWithCacheAsync ( cancelToken ). ConfigureAwait ( false );
2025-02-17 16:45:51 +01:00
await PagedFileListResponse ( currentFolder , false , cancelToken ). LastOrDefaultAsync (). ConfigureAwait ( false );
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 ();
}
2025-02-17 16:45:51 +01:00
private async IAsyncEnumerable < FileEntity > PagedFileListResponse ( string parentid , bool onlyfolders , [ EnumeratorCancellation ] CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
var offset = 0 ;
var done = false ;
if (! onlyfolders )
2018-06-16 11:02:18 -07:00
m_filecache . Clear ();
2024-09-29 22:00:57 +02:00
2015-12-07 09:57:22 +01:00
do
{
2025-02-17 16:45:51 +01:00
var resp = await m_oauth . GetJSONDataAsync < ShortListResponse >( $"{BOX_API_URL}/folders/{parentid}/items?limit={PAGE_SIZE}&offset={offset}&fields=name,size,modified_at" , cancelToken ). ConfigureAwait ( false );
2015-12-07 09:57:22 +01:00
if ( resp . Entries == null || resp . Entries . Length == 0 )
break ;
2024-09-29 22:00:57 +02:00
foreach ( var f in resp . Entries )
2015-12-07 09:57:22 +01:00
{
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 ;
2024-09-29 22:00:57 +02:00
2015-12-07 09:57:22 +01:00
yield return f ;
}
}
offset = offset + PAGE_SIZE ;
if ( offset >= resp . TotalCount )
break ;
2024-09-29 22:00:57 +02:00
} while (! done );
2015-12-07 09:57:22 +01:00
}
#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
{
2024-10-01 09:30:02 +02:00
var currentFolder = await GetCurrentFolderWithCacheAsync ( cancelToken ). ConfigureAwait ( false );
2024-09-29 22:00:57 +02:00
var createreq = new CreateItemRequest ()
{
2015-12-07 09:57:22 +01:00
Name = remotename ,
2024-09-29 22:00:57 +02:00
Parent = new IDReference ()
{
2024-10-01 09:30:02 +02:00
ID = currentFolder
2015-12-07 09:57:22 +01:00
}
};
2018-06-16 11:02:18 -07:00
if ( m_filecache . Count == 0 )
2025-02-17 16:45:51 +01:00
await PagedFileListResponse ( currentFolder , false , cancelToken ). LastOrDefaultAsync (). ConfigureAwait ( false );
2015-12-07 09:57:22 +01:00
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 ;
}
}
2024-09-29 22:00:57 +02:00
public async Task GetAsync ( string remotename , System . IO . Stream stream , CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
2024-10-01 09:30:02 +02:00
var fileId = await GetFileIDAsync ( remotename , cancelToken ). ConfigureAwait ( false );
using ( var resp = await m_oauth . GetResponseAsync ( string . Format ( "{0}/files/{1}/content" , BOX_API_URL , fileId ), cancelToken ). ConfigureAwait ( false ))
2024-09-29 22:00:57 +02:00
using ( var rs = Duplicati . Library . Utility . AsyncHttpRequest . TrySetTimeout ( resp . GetResponseStream ()))
await Library . Utility . Utility . CopyStreamAsync ( rs , stream , cancelToken ). ConfigureAwait ( false );
2015-12-07 09:57:22 +01:00
}
#endregion
#region IBackend implementation
2025-02-17 16:45:51 +01:00
public async IAsyncEnumerable < IFileEntry > ListAsync ([ EnumeratorCancellation ] CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
2025-02-17 16:45:51 +01:00
var currentFolder = await GetCurrentFolderWithCacheAsync ( cancelToken ). ConfigureAwait ( false );
await foreach ( var n in PagedFileListResponse ( currentFolder , false , cancelToken ). ConfigureAwait ( false ))
yield return 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
}
2024-09-29 22:00:57 +02:00
public async Task GetAsync ( string remotename , string filename , CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
using ( System . IO . FileStream fs = System . IO . File . Create ( filename ))
2024-09-29 22:00:57 +02:00
await GetAsync ( remotename , fs , cancelToken ). ConfigureAwait ( false );
2015-12-07 09:57:22 +01:00
}
2024-09-29 23:04:54 +02:00
public async Task DeleteAsync ( string remotename , CancellationToken cancelToken )
2015-12-07 09:57:22 +01:00
{
2024-10-01 09:30:02 +02:00
var fileid = await GetFileIDAsync ( remotename , cancelToken ). ConfigureAwait ( false );
2015-12-07 09:57:22 +01:00
try
{
2024-09-29 23:04:54 +02:00
using ( var r = await m_oauth . GetResponseAsync ( string . Format ( "{0}/files/{1}" , BOX_API_URL , fileid ), cancelToken , null , "DELETE" ). ConfigureAwait ( false ))
2015-12-07 09:57:22 +01:00
{
}
if ( m_deleteFromTrash )
2024-09-29 23:04:54 +02:00
using ( var r = await m_oauth . GetResponseAsync ( string . Format ( "{0}/files/{1}/trash" , BOX_API_URL , fileid ), cancelToken , null , "DELETE" ). ConfigureAwait ( false ))
2015-12-07 09:57:22 +01:00
{
}
}
catch
{
2018-06-16 11:02:18 -07:00
m_filecache . Clear ();
2015-12-07 09:57:22 +01:00
throw ;
}
}
2024-10-01 09:30:02 +02:00
public Task TestAsync ( CancellationToken cancelToken )
2025-02-17 16:45:51 +01:00
=> this . TestListAsync ( cancelToken );
2015-12-07 09:57:22 +01:00
2024-10-01 09:30:02 +02:00
public Task CreateFolderAsync ( CancellationToken cancellationToken )
2015-12-07 09:57:22 +01:00
{
2024-10-01 09:30:02 +02:00
return GetCurrentFolderAsync ( true , cancellationToken );
2015-12-07 09:57:22 +01:00
}
public string DisplayName
{
get
{
return Strings . Box . DisplayName ;
}
}
public string ProtocolKey
{
get
{
return "box" ;
}
}
public IList < ICommandLineArgument > SupportedCommands
{
2024-09-29 22:00:57 +02:00
get
{
2015-12-07 09:57:22 +01:00
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 ;
}
}
2024-10-02 09:37:52 +02:00
public Task < string []> GetDNSNamesAsync ( CancellationToken cancelToken ) => Task . FromResult ( new string [] {
new System . Uri ( BOX_API_URL ). Host ,
new System . Uri ( BOX_UPLOAD_URL ). Host
});
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 ; }
}
}
}