Added File/Container properties windows
[pithos-ms-client] / trunk / Pithos.Network / CloudFilesClient.cs
index c7728e1..889c8dc 100644 (file)
@@ -6,19 +6,16 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel.Composition;
-using System.Diagnostics;
 using System.Diagnostics.Contracts;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
 using System.Security.Cryptography;
 using System.Text;
-using System.Threading.Algorithms;
 using System.Threading.Tasks;
 using Newtonsoft.Json;
 using Pithos.Interfaces;
-using WebHeaderCollection = System.Net.WebHeaderCollection;
+using log4net;
 
 namespace Pithos.Network
 {
@@ -29,12 +26,7 @@ namespace Pithos.Network
         //RestClient provides a REST-friendly interface over the standard WebClient.
         private RestClient _baseClient;
         
-        //Some operations can specify a Timeout. The default value of all timeouts is 10 seconds
-        private readonly TimeSpan _shortTimeout = TimeSpan.FromSeconds(10);
-        
-        //Some operations can be retried before failing. The default number of retries is 5
-        private readonly int _retries = 5;        
-        
+
         //During authentication the client provides a UserName 
         public string UserName { get; set; }
         
@@ -43,13 +35,35 @@ namespace Pithos.Network
         
         //And receives an authentication Token. This token must be provided in ALL other operations,
         //in the X-Auth-Token header
-        public string Token { get; set; }
-        
+        private string _token;
+        public string Token
+        {
+            get { return _token; }
+            set
+            {
+                _token = value;
+                _baseClient.Headers["X-Auth-Token"] = value;
+            }
+        }
+
         //The client also receives a StorageUrl after authentication. All subsequent operations must
         //use this url
         public Uri StorageUrl { get; set; }
-        
-        public Uri Proxy { get; set; }
+
+
+        protected Uri RootAddressUri { get; set; }
+
+        private Uri _proxy;
+        public Uri Proxy
+        {
+            get { return _proxy; }
+            set
+            {
+                _proxy = value;
+                if (_baseClient != null)
+                    _baseClient.Proxy = new WebProxy(value);                
+            }
+        }
 
         public double DownloadPercentLimit { get; set; }
         public double UploadPercentLimit { get; set; }
@@ -64,29 +78,67 @@ namespace Pithos.Network
 
         public bool UsePithos { get; set; }
 
-        private bool _authenticated = false;
-
-        //
-        public void Authenticate(string userName,string apiKey)
-        {
-            Trace.TraceInformation("[AUTHENTICATE] Start for {0}", userName);
-            if (String.IsNullOrWhiteSpace(userName))
-                throw new ArgumentNullException("userName", "The userName property can't be empty");
-            if (String.IsNullOrWhiteSpace(apiKey))
-                throw new ArgumentNullException("apiKey", "The apiKey property can't be empty");
 
-            if (_authenticated)
-                return;
+        private static readonly ILog Log = LogManager.GetLogger("CloudFilesClient");
 
+        public CloudFilesClient(string userName, string apiKey)
+        {
             UserName = userName;
             ApiKey = apiKey;
-            
+        }
+
+        public CloudFilesClient(AccountInfo accountInfo)
+        {
+            if (accountInfo==null)
+                throw new ArgumentNullException("accountInfo");
+            Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
+            Contract.Ensures(StorageUrl != null);
+            Contract.Ensures(_baseClient != null);
+            Contract.Ensures(RootAddressUri != null);
+            Contract.EndContractBlock();
+
+            _baseClient = new RestClient
+            {
+                BaseAddress = accountInfo.StorageUri.ToString(),
+                Timeout = 10000,
+                Retries = 3
+            };
+            StorageUrl = accountInfo.StorageUri;
+            Token = accountInfo.Token;
+            UserName = accountInfo.UserName;
+
+            //Get the root address (StorageUrl without the account)
+            var storageUrl = StorageUrl.AbsoluteUri;
+            var usernameIndex = storageUrl.LastIndexOf(UserName);
+            var rootUrl = storageUrl.Substring(0, usernameIndex);
+            RootAddressUri = new Uri(rootUrl);
+        }
+
+
+        public AccountInfo Authenticate()
+        {
+            if (String.IsNullOrWhiteSpace(UserName))
+                throw new InvalidOperationException("UserName is empty");
+            if (String.IsNullOrWhiteSpace(ApiKey))
+                throw new InvalidOperationException("ApiKey is empty");
+            if (String.IsNullOrWhiteSpace(AuthenticationUrl))
+                throw new InvalidOperationException("AuthenticationUrl is empty");
+            Contract.Ensures(!String.IsNullOrWhiteSpace(Token));
+            Contract.Ensures(StorageUrl != null);
+            Contract.Ensures(_baseClient != null);
+            Contract.Ensures(RootAddressUri != null);
+            Contract.EndContractBlock();
+
+
+            Log.InfoFormat("[AUTHENTICATE] Start for {0}", UserName);
 
             using (var authClient = new RestClient{BaseAddress=AuthenticationUrl})
             {
                 if (Proxy != null)
                     authClient.Proxy = new WebProxy(Proxy);
 
+                Contract.Assume(authClient.Headers!=null);
+
                 authClient.Headers.Add("X-Auth-User", UserName);
                 authClient.Headers.Add("X-Auth-Key", ApiKey);
 
@@ -97,31 +149,44 @@ namespace Pithos.Network
                 var storageUrl = authClient.GetHeaderValue("X-Storage-Url");
                 if (String.IsNullOrWhiteSpace(storageUrl))
                     throw new InvalidOperationException("Failed to obtain storage url");
+                
+                _baseClient = new RestClient
+                {
+                    BaseAddress = storageUrl,
+                    Timeout = 10000,
+                    Retries = 3
+                };
+
                 StorageUrl = new Uri(storageUrl);
                 
+                //Get the root address (StorageUrl without the account)
+                var usernameIndex=storageUrl.LastIndexOf(UserName);
+                var rootUrl = storageUrl.Substring(0, usernameIndex);
+                RootAddressUri = new Uri(rootUrl);
+                
                 var token = authClient.GetHeaderValue("X-Auth-Token");
                 if (String.IsNullOrWhiteSpace(token))
                     throw new InvalidOperationException("Failed to obtain token url");
                 Token = token;
+
             }
 
-            _baseClient = new RestClient{
-                BaseAddress  = StorageUrl.AbsoluteUri,                
-                Timeout=10000,
-                Retries=3};
-            if (Proxy!=null)
-                _baseClient.Proxy = new WebProxy(Proxy);
+            Log.InfoFormat("[AUTHENTICATE] End for {0}", UserName);
+            
 
-            _baseClient.Headers.Add("X-Auth-Token", Token);
+            return new AccountInfo {StorageUri = StorageUrl, Token = Token, UserName = UserName};            
 
-            Trace.TraceInformation("[AUTHENTICATE] End for {0}", userName);
         }
 
 
-        public IList<ContainerInfo> ListContainers()
+
+        public IList<ContainerInfo> ListContainers(string account)
         {
             using (var client = new RestClient(_baseClient))
             {
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+                
                 client.Parameters.Clear();
                 client.Parameters.Add("format", "json");
                 var content = client.DownloadStringWithRetry("", 3);
@@ -130,44 +195,207 @@ namespace Pithos.Network
                 if (client.StatusCode == HttpStatusCode.NoContent)
                     return new List<ContainerInfo>();
                 var infos = JsonConvert.DeserializeObject<IList<ContainerInfo>>(content);
+                
+                foreach (var info in infos)
+                {
+                    info.Account = account;
+                }
                 return infos;
             }
 
         }
 
+        private string GetAccountUrl(string account)
+        {
+            return new Uri(this.RootAddressUri, new Uri(account,UriKind.Relative)).AbsoluteUri;
+        }
+
+        public IList<ShareAccountInfo> ListSharingAccounts(DateTime? since=null)
+        {
+            using (log4net.ThreadContext.Stacks["Share"].Push("List Accounts"))
+            {
+                if (Log.IsDebugEnabled) Log.DebugFormat("START");
+
+                using (var client = new RestClient(_baseClient))
+                {
+                    client.Parameters.Clear();
+                    client.Parameters.Add("format", "json");
+                    client.IfModifiedSince = since;
+
+                    //Extract the username from the base address
+                    client.BaseAddress = RootAddressUri.AbsoluteUri;
+
+                    var content = client.DownloadStringWithRetry(@"", 3);
+
+                    client.AssertStatusOK("ListSharingAccounts failed");
+
+                    //If the result is empty, return an empty list,
+                    var infos = String.IsNullOrWhiteSpace(content)
+                                    ? new List<ShareAccountInfo>()
+                                //Otherwise deserialize the account list into a list of ShareAccountInfos
+                                    : JsonConvert.DeserializeObject<IList<ShareAccountInfo>>(content);
+
+                    Log.DebugFormat("END");
+                    return infos;
+                }
+            }
+        }
+
         //Request listing of all objects in a container modified since a specific time.
         //If the *since* value is missing, return all objects
-        public IList<ObjectInfo> ListObjects(string container, DateTime? since = null)
+        public IList<ObjectInfo> ListSharedObjects(DateTime? since = null)
+        {
+
+            using (log4net.ThreadContext.Stacks["Share"].Push("List Objects"))
+            {
+                if (Log.IsDebugEnabled) Log.DebugFormat("START");
+
+                var objects = new List<ObjectInfo>();
+                var accounts = ListSharingAccounts(since);
+                foreach (var account in accounts)
+                {
+                    var containers = ListContainers(account.name);
+                    foreach (var container in containers)
+                    {
+                        var containerObjects = ListObjects(account.name, container.Name, null);
+                        objects.AddRange(containerObjects);
+                    }
+                }
+                if (Log.IsDebugEnabled) Log.DebugFormat("END");
+                return objects;
+            }
+        }
+
+        public void ShareObject(string account, string container, string objectName, string shareTo, bool read, bool write)
         {
+            if (String.IsNullOrWhiteSpace(Token))
+                throw new InvalidOperationException("The Token is not set");
+            if (StorageUrl==null)
+                throw new InvalidOperationException("The StorageUrl is not set");
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container");
+            if (String.IsNullOrWhiteSpace(objectName))
+                throw new ArgumentNullException("objectName");
+            if (String.IsNullOrWhiteSpace(account))
+                throw new ArgumentNullException("account");
+            if (String.IsNullOrWhiteSpace(shareTo))
+                throw new ArgumentNullException("shareTo");
             Contract.EndContractBlock();
 
-            Trace.TraceInformation("[START] ListObjects");
+            using (log4net.ThreadContext.Stacks["Share"].Push("Share Object"))
+            {
+                if (Log.IsDebugEnabled) Log.DebugFormat("START");
+                
+                using (var client = new RestClient(_baseClient))
+                {
 
-            using (var client = new RestClient(_baseClient))
+                    client.BaseAddress = GetAccountUrl(account);
+
+                    client.Parameters.Clear();
+                    client.Parameters.Add("format", "json");
+
+                    string permission = "";
+                    if (write)
+                        permission = String.Format("write={0}", shareTo);
+                    else if (read)
+                        permission = String.Format("read={0}", shareTo);
+                    client.Headers.Add("X-Object-Sharing", permission);
+
+                    var content = client.DownloadStringWithRetry(container, 3);
+
+                    client.AssertStatusOK("ShareObject failed");
+
+                    //If the result is empty, return an empty list,
+                    var infos = String.IsNullOrWhiteSpace(content)
+                                    ? new List<ObjectInfo>()
+                                //Otherwise deserialize the object list into a list of ObjectInfos
+                                    : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
+
+                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
+                }
+            }
+
+
+        }
+
+        public AccountInfo GetAccountPolicies(AccountInfo accountInfo)
+        {
+            if (accountInfo==null)
+                throw new ArgumentNullException("accountInfo");
+            Contract.EndContractBlock();
+
+            using (log4net.ThreadContext.Stacks["Account"].Push("GetPolicies"))
             {
-                client.Parameters.Clear();
-                client.Parameters.Add("format", "json");
-                client.IfModifiedSince = since;
-                var content = client.DownloadStringWithRetry(container, 3);
+                if (Log.IsDebugEnabled) Log.DebugFormat("START");
+
+                using (var client = new RestClient(_baseClient))
+                {
+                    if (!String.IsNullOrWhiteSpace(accountInfo.UserName))
+                        client.BaseAddress = GetAccountUrl(accountInfo.UserName);
 
-                client.AssertStatusOK("ListObjects failed");
+                    client.Parameters.Clear();
+                    client.Parameters.Add("format", "json");                    
+                    client.Head(String.Empty, 3);
 
-                //If the result is empty, return an empty list,
-                var infos=String.IsNullOrWhiteSpace(content) 
-                    ? new List<ObjectInfo>() 
-                    //Otherwise deserialize the object list into a list of ObjectInfos
-                    : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
+                    var quotaValue=client.ResponseHeaders["X-Account-Policy-Quota"];
+                    var bytesValue= client.ResponseHeaders["X-Account-Bytes-Used"];
+
+                    long quota, bytes;
+                    if (long.TryParse(quotaValue, out quota))
+                        accountInfo.Quota = quota;
+                    if (long.TryParse(bytesValue, out bytes))
+                        accountInfo.BytesUsed = bytes;
+                    
+                    return accountInfo;
+
+                }
 
-                Trace.TraceInformation("[END] ListObjects");
-                return infos;
+            }
+        }
+
+
+        public IList<ObjectInfo> ListObjects(string account, string container, DateTime? since = null)
+        {
+            if (String.IsNullOrWhiteSpace(container))
+                throw new ArgumentNullException("container");
+            Contract.EndContractBlock();
+
+            using (log4net.ThreadContext.Stacks["Objects"].Push("List"))
+            {
+                if (Log.IsDebugEnabled) Log.DebugFormat("START");
+
+                using (var client = new RestClient(_baseClient))
+                {
+                    if (!String.IsNullOrWhiteSpace(account))
+                        client.BaseAddress = GetAccountUrl(account);
+
+                    client.Parameters.Clear();
+                    client.Parameters.Add("format", "json");
+                    client.IfModifiedSince = since;
+                    var content = client.DownloadStringWithRetry(container, 3);
+
+                    client.AssertStatusOK("ListObjects failed");
+
+                    //If the result is empty, return an empty list,
+                    var infos = String.IsNullOrWhiteSpace(content)
+                                    ? new List<ObjectInfo>()
+                                //Otherwise deserialize the object list into a list of ObjectInfos
+                                    : JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
+
+                    foreach (var info in infos)
+                    {
+                        info.Container = container;
+                        info.Account = account;
+                    }
+                    if (Log.IsDebugEnabled) Log.DebugFormat("START");
+                    return infos;
+                }
             }
         }
 
 
 
-        public IList<ObjectInfo> ListObjects(string container, string folder, DateTime? since = null)
+        public IList<ObjectInfo> ListObjects(string account, string container, string folder, DateTime? since = null)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container");
@@ -175,55 +403,83 @@ namespace Pithos.Network
                 throw new ArgumentNullException("folder");
             Contract.EndContractBlock();
 
-            Trace.TraceInformation("[START] ListObjects");
-
-            using (var client = new RestClient(_baseClient))
+            using (log4net.ThreadContext.Stacks["Objects"].Push("List"))
             {
-                client.Parameters.Clear();
-                client.Parameters.Add("format", "json");
-                client.Parameters.Add("path", folder);
-                client.IfModifiedSince = since;
-                var content = client.DownloadStringWithRetry(container, 3);
-                client.AssertStatusOK("ListObjects failed");
+                if (Log.IsDebugEnabled) Log.DebugFormat("START");
 
-                var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
+                using (var client = new RestClient(_baseClient))
+                {
+                    if (!String.IsNullOrWhiteSpace(account))
+                        client.BaseAddress = GetAccountUrl(account);
 
-                Trace.TraceInformation("[END] ListObjects");
-                return infos;
+                    client.Parameters.Clear();
+                    client.Parameters.Add("format", "json");
+                    client.Parameters.Add("path", folder);
+                    client.IfModifiedSince = since;
+                    var content = client.DownloadStringWithRetry(container, 3);
+                    client.AssertStatusOK("ListObjects failed");
+
+                    var infos = JsonConvert.DeserializeObject<IList<ObjectInfo>>(content);
+
+                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
+                    return infos;
+                }
             }
         }
 
  
-        public bool ContainerExists(string container)
+        public bool ContainerExists(string account, string container)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
-            using (var client = new RestClient(_baseClient))
+            Contract.EndContractBlock();
+
+            using (log4net.ThreadContext.Stacks["Containters"].Push("Exists"))
             {
-                client.Parameters.Clear();
-                client.Head(container, 3);
+                if (Log.IsDebugEnabled) Log.DebugFormat("START");
 
-                switch (client.StatusCode)
+                using (var client = new RestClient(_baseClient))
                 {
-                    case HttpStatusCode.OK:
-                    case HttpStatusCode.NoContent:
-                        return true;
-                    case HttpStatusCode.NotFound:
-                        return false;
-                    default:
-                        throw CreateWebException("ContainerExists", client.StatusCode);
+                    if (!String.IsNullOrWhiteSpace(account))
+                        client.BaseAddress = GetAccountUrl(account);
+
+                    client.Parameters.Clear();
+                    client.Head(container, 3);
+                                        
+                    bool result;
+                    switch (client.StatusCode)
+                    {
+                        case HttpStatusCode.OK:
+                        case HttpStatusCode.NoContent:
+                            result=true;
+                            break;
+                        case HttpStatusCode.NotFound:
+                            result=false;
+                            break;
+                        default:
+                            throw CreateWebException("ContainerExists", client.StatusCode);
+                    }
+                    if (Log.IsDebugEnabled) Log.DebugFormat("END");
+
+                    return result;
                 }
+                
             }
         }
 
-        public bool ObjectExists(string container,string objectName)
+        public bool ObjectExists(string account, string container, string objectName)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
             if (String.IsNullOrWhiteSpace(objectName))
                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
+            Contract.EndContractBlock();
+
             using (var client = new RestClient(_baseClient))
             {
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
                 client.Parameters.Clear();
                 client.Head(container + "/" + objectName, 3);
 
@@ -241,83 +497,97 @@ namespace Pithos.Network
 
         }
 
-        public ObjectInfo GetObjectInfo(string container, string objectName)
+        public ObjectInfo GetObjectInfo(string account, string container, string objectName)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
             if (String.IsNullOrWhiteSpace(objectName))
                 throw new ArgumentNullException("objectName", "The objectName property can't be empty");
+            Contract.EndContractBlock();
 
-            using (var client = new RestClient(_baseClient))
-            {
-                try
+            using (log4net.ThreadContext.Stacks["Objects"].Push("GetObjectInfo"))
+            {                
+
+                using (var client = new RestClient(_baseClient))
                 {
-                    client.Parameters.Clear();
+                    if (!String.IsNullOrWhiteSpace(account))
+                        client.BaseAddress = GetAccountUrl(account);
+                    try
+                    {
+                        client.Parameters.Clear();
 
-                    client.Head(container + "/" + objectName, 3);
+                        client.Head(container + "/" + objectName, 3);
 
-                    if (client.TimedOut)
-                        return ObjectInfo.Empty;
+                        if (client.TimedOut)
+                            return ObjectInfo.Empty;
 
-                    switch (client.StatusCode)
+                        switch (client.StatusCode)
+                        {
+                            case HttpStatusCode.OK:
+                            case HttpStatusCode.NoContent:
+                                var keys = client.ResponseHeaders.AllKeys.AsQueryable();
+                                var tags = (from key in keys
+                                            where key.StartsWith("X-Object-Meta-")
+                                            let name = key.Substring(14)
+                                            select new {Name = name, Value = client.ResponseHeaders[name]})
+                                    .ToDictionary(t => t.Name, t => t.Value);
+                                var extensions = (from key in keys
+                                                  where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")
+                                                  select new {Name = key, Value = client.ResponseHeaders[key]})
+                                    .ToDictionary(t => t.Name, t => t.Value);
+                                var info = new ObjectInfo
+                                               {
+                                                   Account = account,
+                                                   Container = container,
+                                                   Name = objectName,
+                                                   Hash = client.GetHeaderValue("ETag"),
+                                                   Content_Type = client.GetHeaderValue("Content-Type"),
+                                                   Bytes = Convert.ToInt64(client.GetHeaderValue("Content-Length")),
+                                                   Tags = tags,
+                                                   Last_Modified = client.LastModified,
+                                                   Extensions = extensions
+                                               };
+                                return info;
+                            case HttpStatusCode.NotFound:
+                                return ObjectInfo.Empty;
+                            default:
+                                throw new WebException(
+                                    String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
+                                                  objectName, client.StatusCode));
+                        }
+
+                    }
+                    catch (RetryException)
                     {
-                        case HttpStatusCode.OK:
-                        case HttpStatusCode.NoContent:
-                            var keys = client.ResponseHeaders.AllKeys.AsQueryable();
-                            var tags = (from key in keys
-                                        where key.StartsWith("X-Object-Meta-")
-                                        let name = key.Substring(14)
-                                        select new {Name = name, Value = client.ResponseHeaders[name]})
-                                .ToDictionary(t => t.Name, t => t.Value);
-                            var extensions = (from key in keys
-                                              where key.StartsWith("X-Object-") && !key.StartsWith("X-Object-Meta-")                                              
-                                              select new {Name = key, Value = client.ResponseHeaders[key]})
-                                .ToDictionary(t => t.Name, t => t.Value);
-                            var info = new ObjectInfo
-                                                 {
-                                                     Name = objectName,
-                                                     Hash = client.GetHeaderValue("ETag"),
-                                                     Content_Type = client.GetHeaderValue("Content-Type"),
-                                                     Tags = tags,
-                                                     Last_Modified = client.LastModified,
-                                                     Extensions = extensions
-                                                 };
-                            return info;
-                        case HttpStatusCode.NotFound:
-                            return ObjectInfo.Empty;
-                        default:
-                            throw new WebException(
-                                String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
-                                              objectName, client.StatusCode));
+                        Log.WarnFormat("[RETRY FAIL] GetObjectInfo for {0} failed.");
+                        return ObjectInfo.Empty;
                     }
-
-                }
-                catch(RetryException e)
-                {
-                    Trace.TraceWarning("[RETRY FAIL] GetObjectInfo for {0} failed.");
-                    return ObjectInfo.Empty;
-                }
-                catch(WebException e)
-                {
-                    Trace.TraceError(
-                        String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
-                                      objectName, client.StatusCode), e);
-                    throw;
-                }
+                    catch (WebException e)
+                    {
+                        Log.Error(
+                            String.Format("[FAIL] GetObjectInfo for {0} failed with unexpected status code {1}",
+                                          objectName, client.StatusCode), e);
+                        throw;
+                    }
+                }                
             }
 
         }
 
-        public void CreateFolder(string container, string folder)
+        public void CreateFolder(string account, string container, string folder)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
             if (String.IsNullOrWhiteSpace(folder))
                 throw new ArgumentNullException("folder", "The folder property can't be empty");
+            Contract.EndContractBlock();
 
             var folderUrl=String.Format("{0}/{1}",container,folder);
             using (var client = new RestClient(_baseClient))
             {
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
                 client.Parameters.Clear();
                 client.Headers.Add("Content-Type", @"application/directory");
                 client.Headers.Add("Content-Length", "0");
@@ -328,12 +598,17 @@ namespace Pithos.Network
             }
         }
 
-        public ContainerInfo GetContainerInfo(string container)
+        public ContainerInfo GetContainerInfo(string account, string container)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
+            Contract.EndContractBlock();
+
             using (var client = new RestClient(_baseClient))
             {
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);                
+
                 client.Head(container);
                 switch (client.StatusCode)
                 {
@@ -341,12 +616,14 @@ namespace Pithos.Network
                     case HttpStatusCode.NoContent:
                         var containerInfo = new ContainerInfo
                                                 {
+                                                    Account=account,
                                                     Name = container,
                                                     Count =
                                                         long.Parse(client.GetHeaderValue("X-Container-Object-Count")),
                                                     Bytes = long.Parse(client.GetHeaderValue("X-Container-Bytes-Used")),
                                                     BlockHash = client.GetHeaderValue("X-Container-Block-Hash"),
-                                                    BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size"))
+                                                    BlockSize=int.Parse(client.GetHeaderValue("X-Container-Block-Size")),
+                                                    Last_Modified=client.LastModified
                                                 };
                         return containerInfo;
                     case HttpStatusCode.NotFound:
@@ -357,12 +634,17 @@ namespace Pithos.Network
             }
         }
 
-        public void CreateContainer(string container)
-        {
+        public void CreateContainer(string account, string container)
+        {            
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
+            Contract.EndContractBlock();
+
             using (var client = new RestClient(_baseClient))
             {
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
                 client.PutWithRetry(container, 3);
                 var expectedCodes = new[] {HttpStatusCode.Created, HttpStatusCode.Accepted, HttpStatusCode.OK};
                 if (!expectedCodes.Contains(client.StatusCode))
@@ -370,12 +652,17 @@ namespace Pithos.Network
             }
         }
 
-        public void DeleteContainer(string container)
+        public void DeleteContainer(string account, string container)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
+            Contract.EndContractBlock();
+
             using (var client = new RestClient(_baseClient))
             {
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
                 client.DeleteWithRetry(container, 3);
                 var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
                 if (!expectedCodes.Contains(client.StatusCode))
@@ -387,6 +674,7 @@ namespace Pithos.Network
         /// <summary>
         /// 
         /// </summary>
+        /// <param name="account"></param>
         /// <param name="container"></param>
         /// <param name="objectName"></param>
         /// <param name="fileName"></param>
@@ -394,7 +682,7 @@ namespace Pithos.Network
         /// <remarks>This method should have no timeout or a very long one</remarks>
         //Asynchronously download the object specified by *objectName* in a specific *container* to 
         // a local file
-        public Task GetObject(string container, string objectName, string fileName)
+        public Task GetObject(string account, string container, string objectName, string fileName)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
@@ -404,20 +692,23 @@ namespace Pithos.Network
 
             try
             {
-                //The container and objectName are relative names. They are joined with the client's
-                //BaseAddress to create the object's absolute address
-                var builder = GetAddressBuilder(container, objectName);
-                var uri = builder.Uri;
                 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
                 //object to avoid concurrency errors.
                 //
                 //Download operations take a long time therefore they have no timeout.
                 var client = new RestClient(_baseClient) { Timeout = 0 };
-               
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
+                //The container and objectName are relative names. They are joined with the client's
+                //BaseAddress to create the object's absolute address
+                var builder = client.GetAddressBuilder(container, objectName);
+                var uri = builder.Uri;
+
                 //Download progress is reported to the Trace log
-                Trace.TraceInformation("[GET] START {0}", objectName);
+                Log.InfoFormat("[GET] START {0}", objectName);
                 client.DownloadProgressChanged += (sender, args) => 
-                    Trace.TraceInformation("[GET PROGRESS] {0} {1}% {2} of {3}",
+                    Log.InfoFormat("[GET PROGRESS] {0} {1}% {2} of {3}",
                                     fileName, args.ProgressPercentage,
                                     args.BytesReceived,
                                     args.TotalBytesToReceive);                                
@@ -434,18 +725,18 @@ namespace Pithos.Network
                                           //And report failure or completion
                                           if (download.IsFaulted)
                                           {
-                                              Trace.TraceError("[GET] FAIL for {0} with \r{1}", objectName,
+                                              Log.ErrorFormat("[GET] FAIL for {0} with \r{1}", objectName,
                                                                download.Exception);
                                           }
                                           else
                                           {
-                                              Trace.TraceInformation("[GET] END {0}", objectName);                                             
+                                              Log.InfoFormat("[GET] END {0}", objectName);                                             
                                           }
                                       });
             }
             catch (Exception exc)
             {
-                Trace.TraceError("[GET] END {0} with {1}", objectName, exc);
+                Log.ErrorFormat("[GET] END {0} with {1}", objectName, exc);
                 throw;
             }
 
@@ -453,7 +744,7 @@ namespace Pithos.Network
 
         }
 
-        public Task<IList<string>> PutHashMap(string container, string objectName, TreeHash hash)
+        public Task<IList<string>> PutHashMap(string account, string container, string objectName, TreeHash hash)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container");
@@ -466,14 +757,19 @@ namespace Pithos.Network
             if (StorageUrl == null)
                 throw new InvalidOperationException("Invalid Storage Url");
             Contract.EndContractBlock();
+
+
+            //Don't use a timeout because putting the hashmap may be a long process
+            var client = new RestClient(_baseClient) { Timeout = 0 };
+            if (!String.IsNullOrWhiteSpace(account))
+                client.BaseAddress = GetAccountUrl(account);
+
             //The container and objectName are relative names. They are joined with the client's
             //BaseAddress to create the object's absolute address
-            var builder = GetAddressBuilder(container, objectName);
+            var builder = client.GetAddressBuilder(container, objectName);
             builder.Query = "format=json&hashmap";
             var uri = builder.Uri;
 
-            //Don't use a timeout because putting the hashmap may be a long process
-            var client = new RestClient(_baseClient) { Timeout = 0 };
 
             //Send the tree hash as Json to the server            
             client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
@@ -526,14 +822,14 @@ namespace Pithos.Network
                 //Any other status code is unexpected but there was no exception. We can probably continue processing
                 else
                 {
-                    Trace.TraceWarning("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);                    
+                    Log.WarnFormat("Unexcpected status code when putting map: {0} - {1}",client.StatusCode,client.StatusDescription);                    
                 }
                 return empty;
             });
 
         }
 
-        public Task<byte[]> GetBlock(string container, Uri relativeUrl, long start, long? end=null)
+        public Task<byte[]> GetBlock(string account, string container, Uri relativeUrl, long start, long? end)
         {
             if (String.IsNullOrWhiteSpace(Token))
                 throw new InvalidOperationException("Invalid Token");
@@ -549,12 +845,15 @@ namespace Pithos.Network
                 throw new ArgumentOutOfRangeException("start");
             Contract.EndContractBlock();
 
-            var builder = GetAddressBuilder(container, relativeUrl.ToString());
-
-            var uri = builder.Uri;
 
             //Don't use a timeout because putting the hashmap may be a long process
             var client = new RestClient(_baseClient) {Timeout = 0, RangeFrom = start, RangeTo = end};
+            if (!String.IsNullOrWhiteSpace(account))
+                client.BaseAddress = GetAccountUrl(account);
+
+            var builder = client.GetAddressBuilder(container, relativeUrl.ToString());
+            var uri = builder.Uri;
+
             return client.DownloadDataTask(uri)
                 .ContinueWith(t=>
                                   {
@@ -564,35 +863,43 @@ namespace Pithos.Network
         }
 
 
-        public Task PostBlock(string container,byte[] block)
+        public Task PostBlock(string account, string container, byte[] block, int offset, int count)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container");
             if (block == null)
                 throw new ArgumentNullException("block");
+            if (offset < 0 || offset >= block.Length)
+                throw new ArgumentOutOfRangeException("offset");
+            if (count < 0 || count > block.Length)
+                throw new ArgumentOutOfRangeException("count");
             if (String.IsNullOrWhiteSpace(Token))
                 throw new InvalidOperationException("Invalid Token");
             if (StorageUrl == null)
-                throw new InvalidOperationException("Invalid Storage Url");            
+                throw new InvalidOperationException("Invalid Storage Url");                        
             Contract.EndContractBlock();
 
-            var builder = GetAddressBuilder(container, "");
+                        
+            //Don't use a timeout because putting the hashmap may be a long process
+            var client = new RestClient(_baseClient) { Timeout = 0 };
+            if (!String.IsNullOrWhiteSpace(account))
+                client.BaseAddress = GetAccountUrl(account);
+
+            var builder = client.GetAddressBuilder(container, "");
             //We are doing an update
             builder.Query = "update";
             var uri = builder.Uri;
-                        
-            //Don't use a timeout because putting the hashmap may be a long process
-            var client = new RestClient(_baseClient) { Timeout = 0 };                                   
+
             client.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
 
-            Trace.TraceInformation("[BLOCK POST] START");
+            Log.InfoFormat("[BLOCK POST] START");
 
             client.UploadProgressChanged += (sender, args) => 
-                Trace.TraceInformation("[BLOCK POST PROGRESS] {0}% {1} of {2}",
+                Log.InfoFormat("[BLOCK POST PROGRESS] {0}% {1} of {2}",
                                     args.ProgressPercentage, args.BytesSent,
                                     args.TotalBytesToSend);
             client.UploadFileCompleted += (sender, args) => 
-                Trace.TraceInformation("[BLOCK POST PROGRESS] Completed ");
+                Log.InfoFormat("[BLOCK POST PROGRESS] Completed ");
 
             
             //Send the block
@@ -604,17 +911,17 @@ namespace Pithos.Network
                 if (upload.IsFaulted)
                 {
                     var exception = upload.Exception.InnerException;
-                    Trace.TraceError("[BLOCK POST] FAIL with \r{0}", exception);                        
+                    Log.ErrorFormat("[BLOCK POST] FAIL with \r{0}", exception);                        
                     throw exception;
                 }
                     
-                Trace.TraceInformation("[BLOCK POST] END");
+                Log.InfoFormat("[BLOCK POST] END");
             });
             return uploadTask;            
         }
 
 
-        public Task<TreeHash> GetHashMap(string container, string objectName)
+        public Task<TreeHash> GetHashMap(string account, string container, string objectName)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container");
@@ -628,19 +935,22 @@ namespace Pithos.Network
 
             try
             {
-                //The container and objectName are relative names. They are joined with the client's
-                //BaseAddress to create the object's absolute address
-                var builder = GetAddressBuilder(container, objectName);
-                builder.Query="format=json&hashmap";
-                var uri = builder.Uri;                
                 //WebClient, and by extension RestClient, are not thread-safe. Create a new RestClient
                 //object to avoid concurrency errors.
                 //
                 //Download operations take a long time therefore they have no timeout.
                 //TODO: Do we really? this is a hashmap operation, not a download
                 var client = new RestClient(_baseClient) { Timeout = 0 };
-               
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
 
+                //The container and objectName are relative names. They are joined with the client's
+                //BaseAddress to create the object's absolute address
+                var builder = client.GetAddressBuilder(container, objectName);
+                builder.Query = "format=json&hashmap";
+                var uri = builder.Uri;
+                
                 //Start downloading the object asynchronously
                 var downloadTask = client.DownloadStringTask(uri);
                 
@@ -652,7 +962,7 @@ namespace Pithos.Network
                     //And report failure or completion
                     if (download.IsFaulted)
                     {
-                        Trace.TraceError("[GET HASH] FAIL for {0} with \r{1}", objectName,
+                        Log.ErrorFormat("[GET HASH] FAIL for {0} with \r{1}", objectName,
                                         download.Exception);
                         throw download.Exception;
                     }
@@ -660,13 +970,13 @@ namespace Pithos.Network
                     //The server will return an empty string if the file is empty
                     var json = download.Result;
                     var treeHash = TreeHash.Parse(json);
-                    Trace.TraceInformation("[GET HASH] END {0}", objectName);                                             
+                    Log.InfoFormat("[GET HASH] END {0}", objectName);                                             
                     return treeHash;
                 });
             }
             catch (Exception exc)
             {
-                Trace.TraceError("[GET HASH] END {0} with {1}", objectName, exc);
+                Log.ErrorFormat("[GET HASH] END {0} with {1}", objectName, exc);
                 throw;
             }
 
@@ -674,22 +984,17 @@ namespace Pithos.Network
 
         }
 
-        private UriBuilder GetAddressBuilder(string container, string objectName)
-        {
-            var builder = new UriBuilder(String.Join("/", _baseClient.BaseAddress, container, objectName));
-            return builder;
-        }
-
 
         /// <summary>
         /// 
         /// </summary>
+        /// <param name="account"></param>
         /// <param name="container"></param>
         /// <param name="objectName"></param>
         /// <param name="fileName"></param>
         /// <param name="hash">Optional hash value for the file. If no hash is provided, the method calculates a new hash</param>
         /// <remarks>>This method should have no timeout or a very long one</remarks>
-        public Task PutObject(string container, string objectName, string fileName, string hash = null)
+        public Task PutObject(string account, string container, string objectName, string fileName, string hash = null)
         {
             if (String.IsNullOrWhiteSpace(container))
                 throw new ArgumentNullException("container", "The container property can't be empty");
@@ -699,29 +1004,40 @@ namespace Pithos.Network
                 throw new ArgumentNullException("fileName", "The fileName property can't be empty");
             if (!File.Exists(fileName))
                 throw new FileNotFoundException("The file does not exist",fileName);
-
+            Contract.EndContractBlock();
             
             try
             {
-                var builder= GetAddressBuilder(container,objectName);
+
+                var client = new RestClient(_baseClient){Timeout=0};
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
+                var builder = client.GetAddressBuilder(container, objectName);
                 var uri = builder.Uri;
 
-                var client = new RestClient(_baseClient){Timeout=0};           
                 string etag = hash ?? CalculateHash(fileName);
 
                 client.Headers.Add("Content-Type", "application/octet-stream");
                 client.Headers.Add("ETag", etag);
 
 
-                Trace.TraceInformation("[PUT] START {0}", objectName);
+                Log.InfoFormat("[PUT] START {0}", objectName);
                 client.UploadProgressChanged += (sender, args) =>
                 {
-                    Trace.TraceInformation("[PUT PROGRESS] {0} {1}% {2} of {3}", fileName, args.ProgressPercentage, args.BytesSent, args.TotalBytesToSend);
+                    using (log4net.ThreadContext.Stacks["PUT"].Push("Progress"))
+                    {
+                        Log.InfoFormat("{0} {1}% {2} of {3}", fileName, args.ProgressPercentage,
+                                       args.BytesSent, args.TotalBytesToSend);
+                    }
                 };
 
                 client.UploadFileCompleted += (sender, args) =>
                 {
-                    Trace.TraceInformation("[PUT PROGRESS] Completed {0}", fileName);
+                    using (log4net.ThreadContext.Stacks["PUT"].Push("Progress"))
+                    {
+                        Log.InfoFormat("Completed {0}", fileName);
+                    }
                 };
                 return client.UploadFileTask(uri, "PUT", fileName)
                     .ContinueWith(upload=>
@@ -731,16 +1047,16 @@ namespace Pithos.Network
                                           if (upload.IsFaulted)
                                           {
                                               var exc = upload.Exception.InnerException;
-                                              Trace.TraceError("[PUT] FAIL for {0} with \r{1}",objectName,exc);
+                                              Log.ErrorFormat("[PUT] FAIL for {0} with \r{1}",objectName,exc);
                                               throw exc;
                                           }
                                           else
-                                            Trace.TraceInformation("[PUT] END {0}", objectName);
+                                            Log.InfoFormat("[PUT] END {0}", objectName);
                                       });
             }
             catch (Exception exc)
             {
-                Trace.TraceError("[PUT] END {0} with {1}", objectName, exc);
+                Log.ErrorFormat("[PUT] END {0} with {1}", objectName, exc);
                 throw;
             }                
 
@@ -749,6 +1065,9 @@ namespace Pithos.Network
         
         private static string CalculateHash(string fileName)
         {
+            Contract.Requires(!String.IsNullOrWhiteSpace(fileName));
+            Contract.EndContractBlock();
+
             string hash;
             using (var hasher = MD5.Create())
             using(var stream=File.OpenRead(fileName))
@@ -760,26 +1079,8 @@ namespace Pithos.Network
             }
             return hash;
         }
-
-        public void DeleteObject(string container, string objectName)
-        {
-            if (String.IsNullOrWhiteSpace(container))
-                throw new ArgumentNullException("container", "The container property can't be empty");
-            if (String.IsNullOrWhiteSpace(objectName))
-                throw new ArgumentNullException("objectName", "The objectName property can't be empty");
-            using (var client = new RestClient(_baseClient))
-            {
-
-                client.DeleteWithRetry(container + "/" + objectName, 3);
-
-                var expectedCodes = new[] {HttpStatusCode.NotFound, HttpStatusCode.NoContent};
-                if (!expectedCodes.Contains(client.StatusCode))
-                    throw CreateWebException("DeleteObject", client.StatusCode);
-            }
-
-        }
-
-        public void MoveObject(string sourceContainer, string oldObjectName, string targetContainer,string newObjectName)
+        
+        public void MoveObject(string account, string sourceContainer, string oldObjectName, string targetContainer, string newObjectName)
         {
             if (String.IsNullOrWhiteSpace(sourceContainer))
                 throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
@@ -789,25 +1090,51 @@ namespace Pithos.Network
                 throw new ArgumentNullException("targetContainer", "The container property can't be empty");
             if (String.IsNullOrWhiteSpace(newObjectName))
                 throw new ArgumentNullException("newObjectName", "The newObjectName property can't be empty");
+            Contract.EndContractBlock();
 
             var targetUrl = targetContainer + "/" + newObjectName;
             var sourceUrl = String.Format("/{0}/{1}", sourceContainer, oldObjectName);
 
             using (var client = new RestClient(_baseClient))
             {
-                client.Headers.Add("X-Copy-From", sourceUrl);
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
+                client.Headers.Add("X-Move-From", sourceUrl);
                 client.PutWithRetry(targetUrl, 3);
 
                 var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created};
-                if (expectedCodes.Contains(client.StatusCode))
-                {
-                    this.DeleteObject(sourceContainer, oldObjectName);
-                }
-                else
+                if (!expectedCodes.Contains(client.StatusCode))
                     throw CreateWebException("MoveObject", client.StatusCode);
             }
         }
 
+        public void DeleteObject(string account, string sourceContainer, string objectName)
+        {            
+            if (String.IsNullOrWhiteSpace(sourceContainer))
+                throw new ArgumentNullException("sourceContainer", "The container property can't be empty");
+            if (String.IsNullOrWhiteSpace(objectName))
+                throw new ArgumentNullException("objectName", "The oldObjectName property can't be empty");
+            Contract.EndContractBlock();
+
+            var targetUrl = FolderConstants.TrashContainer + "/" + objectName;
+            var sourceUrl = String.Format("/{0}/{1}", sourceContainer, objectName);
+
+            using (var client = new RestClient(_baseClient))
+            {
+                if (!String.IsNullOrWhiteSpace(account))
+                    client.BaseAddress = GetAccountUrl(account);
+
+                client.Headers.Add("X-Move-From", sourceUrl);
+                client.AllowedStatusCodes.Add(HttpStatusCode.NotFound);
+                client.PutWithRetry(targetUrl, 3);
+
+                var expectedCodes = new[] {HttpStatusCode.OK, HttpStatusCode.NoContent, HttpStatusCode.Created,HttpStatusCode.NotFound};
+                if (!expectedCodes.Contains(client.StatusCode))
+                    throw CreateWebException("DeleteObject", client.StatusCode);
+            }
+        }
+
       
         private static WebException CreateWebException(string operation, HttpStatusCode statusCode)
         {
@@ -816,4 +1143,10 @@ namespace Pithos.Network
 
         
     }
+
+    public class ShareAccountInfo
+    {
+        public DateTime? last_modified { get; set; }
+        public string name { get; set; }
+    }
 }