2 using System.ComponentModel.Composition;
3 using System.Diagnostics;
4 using System.Diagnostics.Contracts;
7 using System.Reflection;
8 using System.Threading.Tasks;
9 using Pithos.Interfaces;
13 namespace Pithos.Core.Agents
15 [Export(typeof(Uploader))]
18 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
21 private IStatusKeeper StatusKeeper { get; set; }
24 public IStatusNotification StatusNotification { get; set; }
26 public async Task UploadCloudFile(CloudAction action)
29 throw new ArgumentNullException("action");
30 Contract.EndContractBlock();
32 using (ThreadContext.Stacks["Operation"].Push("UploadCloudFile"))
36 var fileInfo = action.LocalFile;
38 if (fileInfo.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase))
41 //Try to load the action's local state, if it is empty
42 if (action.FileState == null)
43 action.FileState = StatusKeeper.GetStateByFilePath(fileInfo.FullName);
44 if (action.FileState == null)
46 Log.WarnFormat("File [{0}] has no local state. It was probably created by a download action", fileInfo.FullName);
50 var latestState = action.FileState;
52 //Do not upload files in conflict
53 if (latestState.FileStatus == FileStatus.Conflict)
55 Log.InfoFormat("Skipping file in conflict [{0}]", fileInfo.FullName);
58 //Do not upload files when we have no permission
59 if (latestState.FileStatus == FileStatus.Forbidden)
61 Log.InfoFormat("Skipping forbidden file [{0}]", fileInfo.FullName);
65 //Are we targeting our own account or a sharer account?
66 var relativePath = fileInfo.AsRelativeTo(action.AccountInfo.AccountPath);
67 var accountInfo = relativePath.StartsWith(FolderConstants.OthersFolder)
68 ? GetSharerAccount(relativePath, action.AccountInfo)
73 var fullFileName = fileInfo.GetProperCapitalization();
74 using (var gate = NetworkGate.Acquire(fullFileName, NetworkOperation.Uploading))
76 //Abort if the file is already being uploaded or downloaded
80 var cloudFile = action.CloudFile;
81 var account = cloudFile.Account ?? accountInfo.UserName;
85 var client = new CloudFilesClient(accountInfo);
86 //Even if GetObjectInfo times out, we can proceed with the upload
87 var cloudInfo = client.GetObjectInfo(account, cloudFile.Container, cloudFile.Name);
89 //If this is a read-only file, do not upload changes
90 if (!cloudInfo.IsWritable(action.AccountInfo.UserName))
93 //TODO: If the object does not exist, check that we can upload to the folder
95 if (fileInfo is DirectoryInfo)
97 //If the directory doesn't exist the Hash property will be empty
98 if (String.IsNullOrWhiteSpace(cloudInfo.Hash))
99 //Go on and create the directory
100 await client.PutObject(account, cloudFile.Container, cloudFile.Name, fullFileName,
101 String.Empty, "application/directory");
106 var cloudHash = cloudInfo.Hash.ToLower();
110 using(StatusNotification.GetNotifier("Hashing {0} for Upload", "Finished hashing {0}",fileInfo.Name))
112 treeHash = action.TreeHash.Value;
113 topHash = treeHash.TopHash.ToHashString();
116 //If the file hashes match, abort the upload
117 if (cloudInfo != ObjectInfo.Empty && topHash == cloudHash)
119 //but store any metadata changes
120 StatusKeeper.StoreInfo(fullFileName, cloudInfo);
121 Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName);
126 //Mark the file as modified while we upload it
127 StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);
130 //Upload even small files using the Hashmap. The server may already contain
133 await UploadWithHashMap(accountInfo, cloudFile, fileInfo as FileInfo, cloudFile.Name, treeHash);
135 //If everything succeeds, change the file and overlay status to normal
136 StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");
138 catch (WebException exc)
140 var response = (exc.Response as HttpWebResponse);
141 if (response == null)
143 if (response.StatusCode == HttpStatusCode.Forbidden)
145 StatusKeeper.SetFileState(fileInfo.FullName, FileStatus.Forbidden, FileOverlayStatus.Conflict, "Forbidden");
149 //In any other case, propagate the error
153 //Notify the Shell to update the overlays
154 NativeMethods.RaiseChangeNotification(fullFileName);
155 StatusNotification.NotifyChangedFile(fullFileName);
157 catch (AggregateException ex)
159 var exc = ex.InnerException as WebException;
161 throw ex.InnerException;
162 if (HandleUploadWebException(action, exc))
166 catch (WebException ex)
168 if (HandleUploadWebException(action, ex))
174 Log.Error("Unexpected error while uploading file", ex);
180 private static AccountInfo GetSharerAccount(string relativePath, AccountInfo accountInfo)
182 var parts = relativePath.Split('\\');
183 var accountName = parts[1];
184 var oldName = accountInfo.UserName;
185 var absoluteUri = accountInfo.StorageUri.AbsoluteUri;
186 var nameIndex = absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
187 var root = absoluteUri.Substring(0, nameIndex);
189 accountInfo = new AccountInfo
191 UserName = accountName,
192 AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
193 StorageUri = new Uri(root + accountName),
194 BlockHash = accountInfo.BlockHash,
195 BlockSize = accountInfo.BlockSize,
196 Token = accountInfo.Token
202 public async Task UploadWithHashMap(AccountInfo accountInfo, ObjectInfo cloudFile, FileInfo fileInfo, string url, TreeHash treeHash)
204 if (accountInfo == null)
205 throw new ArgumentNullException("accountInfo");
206 if (cloudFile == null)
207 throw new ArgumentNullException("cloudFile");
208 if (fileInfo == null)
209 throw new ArgumentNullException("fileInfo");
210 if (String.IsNullOrWhiteSpace(url))
211 throw new ArgumentNullException(url);
212 if (treeHash == null)
213 throw new ArgumentNullException("treeHash");
214 if (String.IsNullOrWhiteSpace(cloudFile.Container))
215 throw new ArgumentException("Invalid container", "cloudFile");
216 Contract.EndContractBlock();
218 using (StatusNotification.GetNotifier("Uploading {0}", "Finished Uploading {0}", fileInfo.Name))
221 var fullFileName = fileInfo.GetProperCapitalization();
223 var account = cloudFile.Account ?? accountInfo.UserName;
224 var container = cloudFile.Container;
226 var client = new CloudFilesClient(accountInfo);
227 //Send the hashmap to the server
228 var missingHashes = await client.PutHashMap(account, container, url, treeHash);
230 ReportUploadProgress(fileInfo.Name, block++, missingHashes.Count, fileInfo.Length);
231 //If the server returns no missing hashes, we are done
232 while (missingHashes.Count > 0)
235 var buffer = new byte[accountInfo.BlockSize];
236 foreach (var missingHash in missingHashes)
238 //Find the proper block
239 var blockIndex = treeHash.HashDictionary[missingHash];
240 long offset = blockIndex*accountInfo.BlockSize;
242 var read = fileInfo.Read(buffer, offset, accountInfo.BlockSize);
246 //And upload the block
247 await client.PostBlock(account, container, buffer, 0, read);
248 Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex, fullFileName);
250 catch (Exception exc)
252 Log.Error(String.Format("Uploading block {0} of {1}", blockIndex, fullFileName), exc);
254 ReportUploadProgress(fileInfo.Name, block++, missingHashes.Count, fileInfo.Length);
257 //Repeat until there are no more missing hashes
258 missingHashes = await client.PutHashMap(account, container, url, treeHash);
261 ReportUploadProgress(fileInfo.Name, missingHashes.Count, missingHashes.Count, fileInfo.Length);
265 private void ReportUploadProgress(string fileName, int block, int totalBlocks, long fileSize)
267 StatusNotification.Notify(totalBlocks == 0
268 ? new ProgressNotification(fileName, "Uploading", 1, 1, fileSize)
269 : new ProgressNotification(fileName, "Uploading", block, totalBlocks, fileSize));
273 private bool HandleUploadWebException(CloudAction action, WebException exc)
275 var response = exc.Response as HttpWebResponse;
276 if (response == null)
278 if (response.StatusCode == HttpStatusCode.Unauthorized)
280 Log.Error("Not allowed to upload file", exc);
281 var message = String.Format("Not allowed to uplad file {0}", action.LocalFile.FullName);
282 StatusKeeper.SetFileState(action.LocalFile.FullName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");
283 StatusNotification.NotifyChange(message, TraceLevel.Warning);