using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Net; using Duplicati.Library.Interface; using Duplicati.Library.Localization.Short; namespace Duplicati.Library.Backend { public class SkyDrive : IBackend, IStreamingBackend { private const string AUTHID_OPTION = "authid"; private const string WLID_SERVICE = "https://duplicati-oauth-handler.appspot.com/refresh"; private const string WLID_LOGIN = "https://duplicati-oauth-handler.appspot.com/"; private const string WLID_SERVER = "https://apis.live.net/v5.0"; private const string ROOT_FOLDER_ID = "me/skydrive"; private const string FOLDER_TEMPLATE = "{0}/files"; private static readonly string USER_AGENT = string.Format("Duplicati v{0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()); private string m_authid; private string m_rootfolder; private string m_prefix; private string m_token; private DateTime m_tokenExpires = DateTime.UtcNow; private WLID_FolderItem m_folderid; private Dictionary m_fileidCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); public SkyDrive() { } public SkyDrive(string url, Dictionary options) { var uri = new Utility.Uri(url); m_rootfolder = uri.Host; m_prefix = "/" + uri.Path; if (!m_prefix.EndsWith("/")) m_prefix += "/"; if (options.ContainsKey(AUTHID_OPTION)) m_authid = options[AUTHID_OPTION]; if (string.IsNullOrEmpty(m_authid)) throw new Exception(string.Format(LC.L("You need an AuthID, you can get it from: {0}"), WLID_LOGIN)); } private class WLID_Service_Response { public string access_token { get; set; } [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public int expires { get; set; } } private class WLID_DataItem { public WLID_FolderItem[] data { get; set; } } private class WLID_FolderItem { public string id { get; set; } public string name { get; set; } public string description { get; set; } public string parent_id { get; set; } public string upload_location { get; set; } public string type { get; set; } [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public DateTime created_time { get; set; } [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public DateTime updated_time { get; set; } [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public long size { get; set; } } private class WLID_CreateFolderData { public string name; public string description; } private T GetJSONData(string url, Action setup = null) { var req = (HttpWebRequest)System.Net.WebRequest.Create(url); req.UserAgent = USER_AGENT; if (setup != null) setup(req); Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req); using(var resp = (HttpWebResponse)areq.GetResponse()) using(var rs = areq.GetResponseStream()) using(var tr = new System.IO.StreamReader(rs)) using(var jr = new Newtonsoft.Json.JsonTextReader(tr)) return new Newtonsoft.Json.JsonSerializer().Deserialize(jr); } private string AccessToken { get { if (m_token == null || m_tokenExpires < DateTime.UtcNow) { try { var res = GetJSONData(WLID_SERVICE, (req) => { req.Headers.Add("X-AuthID", m_authid); }); m_tokenExpires = DateTime.UtcNow.AddSeconds(res.expires - 30); m_token = res.access_token; } catch (Exception ex) { var msg = ex.Message; if (ex is WebException) { var resp = ((WebException)ex).Response as HttpWebResponse; if (resp != null) { msg = resp.Headers["X-Reason"]; if (string.IsNullOrWhiteSpace(msg)) msg = resp.StatusDescription; } } throw new Exception(LC.L("Failed to authorize using the WLID service: {0}. If the problem persists, try generating a new authid token from: {1}", msg, WLID_LOGIN), ex); } } return m_token; } } private WLID_FolderItem FindFolder(string folder, string parentfolder = null) { if (string.IsNullOrWhiteSpace(parentfolder)) parentfolder = ROOT_FOLDER_ID; var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, string.Format(FOLDER_TEMPLATE, parentfolder), Library.Utility.Uri.UrlEncode(AccessToken)); var res = GetJSONData(url); if (res == null || res.data == null) return null; foreach(var r in res.data) if (string.Equals(r.name, folder, StringComparison.InvariantCultureIgnoreCase)) return r; return null; } private WLID_FolderItem FindFolders(bool autocreate) { var folders = (m_rootfolder + '/' + m_prefix).Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (folders.Length == 0) { var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, ROOT_FOLDER_ID, Library.Utility.Uri.UrlEncode(AccessToken)); return GetJSONData(url); } WLID_FolderItem cur = null; foreach (var f in folders) { var n = FindFolder(f, cur == null ? null : cur.id); if (n == null) { if (autocreate) { var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, cur == null ? ROOT_FOLDER_ID : cur.id, Library.Utility.Uri.UrlEncode(AccessToken)); var req = (HttpWebRequest)WebRequest.Create(url); req.UserAgent = USER_AGENT; req.Method = "POST"; var areq = new Utility.AsyncHttpRequest(req); using (var ms = new System.IO.MemoryStream()) using (var sw = new System.IO.StreamWriter(ms)) { new Newtonsoft.Json.JsonSerializer().Serialize(sw, new WLID_CreateFolderData() { name = f, description = LC.L("Autocreated folder") }); sw.Flush(); ms.Position = 0; req.ContentLength = ms.Length; req.ContentType = "application/json"; using (var reqs = areq.GetRequestStream()) Utility.Utility.CopyStream(ms, reqs); } using (var resp = (HttpWebResponse)areq.GetResponse()) using (var rs = areq.GetResponseStream()) using (var tr = new System.IO.StreamReader(rs)) using (var jr = new Newtonsoft.Json.JsonTextReader(tr)) { if ((int)resp.StatusCode < 200 || (int)resp.StatusCode > 299) throw new ProtocolViolationException(string.Format(LC.L("Unexpected error code: {0} - {1}"), resp.StatusCode, resp.StatusDescription)); cur = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); } } else throw new FolderMissingException(LC.L("Missing the folder: {0}", f)); } else cur = n; } return cur; } private string FolderID { get { if (m_folderid == null) m_folderid = FindFolders(false); return m_folderid.id; } } private string GetFileID(string name, bool throwIfMissing = true) { string id; m_fileidCache.TryGetValue(name, out id); if (string.IsNullOrWhiteSpace(id)) { // Refresh the list of files, just in case List(); m_fileidCache.TryGetValue(name, out id); if (string.IsNullOrWhiteSpace(id) && throwIfMissing) throw new System.IO.FileNotFoundException(LC.L("File not found"), name); } return id; } #region IBackend Members public void Test() { List(); } public void CreateFolder() { FindFolders(true); } public string DisplayName { get { return LC.L("Microsoft OneDrive"); } } public string ProtocolKey { get { return "onedrive"; } } public List List() { var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, string.Format(FOLDER_TEMPLATE, FolderID), Library.Utility.Uri.UrlEncode(AccessToken)); var res = GetJSONData(url); var files = new List(); m_fileidCache.Clear(); if (res != null && res.data != null) foreach (var r in res.data) { m_fileidCache.Add(r.name, r.id); var fe = new FileEntry(r.name, r.size, r.updated_time, r.updated_time); fe.IsFolder = string.Equals(r.type, "folder", StringComparison.InvariantCultureIgnoreCase); files.Add(fe); } return files; } 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 id = GetFileID(remotename); var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, id, Library.Utility.Uri.UrlEncode(AccessToken)); var req = (HttpWebRequest)WebRequest.Create(url); req.Method = "DELETE"; var areq = new Utility.AsyncHttpRequest(req); using (var resp = (HttpWebResponse)areq.GetResponse()) { if ((int)resp.StatusCode < 200 || (int)resp.StatusCode > 299) throw new ProtocolViolationException(string.Format(LC.L("Unexpected error code: {0} - {1}"), resp.StatusCode, resp.StatusDescription)); m_fileidCache.Remove(remotename); } } public IList SupportedCommands { get { return new List(new ICommandLineArgument[] { new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, LC.L("The authorization code"), string.Format(LC.L("The authorization token retrieved from {0}", WLID_LOGIN))), }); } } public string Description { get { return LC.L("Stores files on Microsoft OneDrive. Usage of this backend requires that you agree to the terms in {0} ({1}) and {2} ({3})", "Microsoft Service Agreement", "http://explore.live.com/microsoft-service-agreement", "Microsoft Online Privacy Statement", "http://privacy.microsoft.com/en-us/fullnotice.mspx" ); } } #endregion #region IDisposable Members public void Dispose() { } #endregion #region IStreamingBackend Members public void Put(string remotename, System.IO.Stream stream) { var url = string.Format("{0}/{1}/files/{2}?access_token={3}", WLID_SERVER, FolderID, Utility.Uri.UrlPathEncode(remotename), Library.Utility.Uri.UrlEncode(AccessToken)); var req = (HttpWebRequest)WebRequest.Create(url); req.UserAgent = USER_AGENT; req.Method = "PUT"; try { req.ContentLength = stream.Length; } catch { } // Docs says not to set this ? //req.ContentType = "application/octet-stream"; var areq = new Utility.AsyncHttpRequest(req); using (var reqs = areq.GetRequestStream()) Utility.Utility.CopyStream(stream, reqs); using (var resp = (HttpWebResponse)areq.GetResponse()) using (var rs = areq.GetResponseStream()) using (var tr = new System.IO.StreamReader(rs)) using (var jr = new Newtonsoft.Json.JsonTextReader(tr)) { var nf = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); m_fileidCache[remotename] = nf.id; } } public void Get(string remotename, System.IO.Stream stream) { var id = GetFileID(remotename); var url = string.Format("{0}/{1}/content?access_token={2}", WLID_SERVER, id, Library.Utility.Uri.UrlEncode(AccessToken)); var req = (HttpWebRequest)WebRequest.Create(url); req.UserAgent = USER_AGENT; var areq = new Utility.AsyncHttpRequest(req); using (var resp = (HttpWebResponse)areq.GetResponse()) using (var rs = areq.GetResponseStream()) Utility.Utility.CopyStream(rs, stream); } #endregion } }