2 using System.Collections.Generic;
3 using System.ComponentModel.Composition;
4 using System.Diagnostics;
5 using System.Diagnostics.Contracts;
9 using System.Reflection;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Pithos.Interfaces;
16 namespace Pithos.Core.Agents
18 [Export(typeof(Uploader))]
21 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
24 private IStatusKeeper StatusKeeper { get; set; }
27 public IStatusNotification StatusNotification { get; set; }
30 //CancellationTokenSource _cts = new CancellationTokenSource();
31 /*public void SignalStop()
36 public async Task UploadCloudFile(CloudAction action,CancellationToken cancellationToken)
39 throw new ArgumentNullException("action");
40 Contract.EndContractBlock();
42 using (ThreadContext.Stacks["Operation"].Push("UploadCloudFile"))
46 await UnpauseEvent.WaitAsync();
48 var fileInfo = action.LocalFile;
50 if (fileInfo.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase))
53 if (Selectives.IsSelected(action.AccountInfo, fileInfo))
56 //Try to load the action's local state, if it is empty
57 if (action.FileState == null)
58 action.FileState = StatusKeeper.GetStateByFilePath(fileInfo.FullName);
59 if (action.FileState == null)
61 Log.WarnFormat("File [{0}] has no local state. It was probably created by a download action", fileInfo.FullName);
65 var latestState = action.FileState;
67 //Do not upload files in conflict
68 if (latestState.FileStatus == FileStatus.Conflict)
70 Log.InfoFormat("Skipping file in conflict [{0}]", fileInfo.FullName);
73 //Do not upload files when we have no permission
74 if (latestState.FileStatus == FileStatus.Forbidden)
76 Log.InfoFormat("Skipping forbidden file [{0}]", fileInfo.FullName);
80 //Are we targeting our own account or a sharer account?
81 var relativePath = fileInfo.AsRelativeTo(action.AccountInfo.AccountPath);
82 var accountInfo = relativePath.StartsWith(FolderConstants.OthersFolder)
83 ? GetSharerAccount(relativePath, action.AccountInfo)
88 var fullFileName = fileInfo.GetProperCapitalization();
89 using (var gate = NetworkGate.Acquire(fullFileName, NetworkOperation.Uploading))
91 //Abort if the file is already being uploaded or downloaded
95 var cloudFile = action.CloudFile;
96 var account = cloudFile.Account ?? accountInfo.UserName;
100 var client = new CloudFilesClient(accountInfo);
102 //Even if GetObjectInfo times out, we can proceed with the upload
103 var cloudInfo = client.GetObjectInfo(account, cloudFile.Container, cloudFile.Name);
105 //If this a shared file
106 if (!cloudFile.Account.Equals(action.AccountInfo.UserName,StringComparison.InvariantCultureIgnoreCase))
108 //If this is a read-only file, do not upload changes
109 if (!cloudInfo.IsWritable(action.AccountInfo.UserName))
111 MakeFileReadOnly(fullFileName);
115 //If the file is new, can we upload it?
116 if ( !cloudInfo.Exists && !client.CanUpload(account, cloudFile))
118 MakeFileReadOnly(fullFileName);
124 await UnpauseEvent.WaitAsync();
126 if (fileInfo is DirectoryInfo)
128 //If the directory doesn't exist the Hash property will be empty
129 if (String.IsNullOrWhiteSpace(cloudInfo.Hash))
130 //Go on and create the directory
131 await client.PutObject(account, cloudFile.Container, cloudFile.Name, fullFileName,
132 String.Empty, "application/directory");
137 var cloudHash = cloudInfo.Hash.ToLower();
141 using(StatusNotification.GetNotifier("Hashing {0} for Upload", "Finished hashing {0}",fileInfo.Name))
143 treeHash = action.TreeHash.Value;
144 topHash = treeHash.TopHash.ToHashString();
147 //If the file hashes match, abort the upload
148 if (cloudInfo != ObjectInfo.Empty && topHash == cloudHash)
150 //but store any metadata changes
151 StatusKeeper.StoreInfo(fullFileName, cloudInfo);
152 Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName);
157 //Mark the file as modified while we upload it
158 StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);
161 //Upload even small files using the Hashmap. The server may already contain
166 await UploadWithHashMap(accountInfo, cloudFile, fileInfo as FileInfo, cloudFile.Name, treeHash,cancellationToken);
168 //If everything succeeds, change the file and overlay status to normal
169 StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");
171 catch (WebException exc)
173 var response = (exc.Response as HttpWebResponse);
174 if (response == null)
176 if (response.StatusCode == HttpStatusCode.Forbidden)
178 StatusKeeper.SetFileState(fileInfo.FullName, FileStatus.Forbidden, FileOverlayStatus.Conflict, "Forbidden");
179 MakeFileReadOnly(fullFileName);
182 //In any other case, propagate the error
186 //Notify the Shell to update the overlays
187 NativeMethods.RaiseChangeNotification(fullFileName);
188 StatusNotification.NotifyChangedFile(fullFileName);
190 catch (AggregateException ex)
192 var exc = ex.InnerException as WebException;
194 throw ex.InnerException;
195 if (HandleUploadWebException(action, exc))
199 catch (WebException ex)
201 if (HandleUploadWebException(action, ex))
207 Log.Error("Unexpected error while uploading file", ex);
213 private static void MakeFileReadOnly(string fullFileName)
215 var attributes = File.GetAttributes(fullFileName);
216 //Do not make any modifications if not necessary
217 if (attributes.HasFlag(FileAttributes.ReadOnly))
219 File.SetAttributes(fullFileName, attributes | FileAttributes.ReadOnly);
222 private static AccountInfo GetSharerAccount(string relativePath, AccountInfo accountInfo)
224 var parts = relativePath.Split('\\');
225 var accountName = parts[1];
226 var oldName = accountInfo.UserName;
227 var absoluteUri = accountInfo.StorageUri.AbsoluteUri;
228 var nameIndex = absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
229 var root = absoluteUri.Substring(0, nameIndex);
231 accountInfo = new AccountInfo
233 UserName = accountName,
234 AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
235 StorageUri = new Uri(root + accountName),
236 BlockHash = accountInfo.BlockHash,
237 BlockSize = accountInfo.BlockSize,
238 Token = accountInfo.Token
244 public async Task UploadWithHashMap(AccountInfo accountInfo, ObjectInfo cloudFile, FileInfo fileInfo, string url, TreeHash treeHash, CancellationToken token)
246 if (accountInfo == null)
247 throw new ArgumentNullException("accountInfo");
248 if (cloudFile == null)
249 throw new ArgumentNullException("cloudFile");
250 if (fileInfo == null)
251 throw new ArgumentNullException("fileInfo");
252 if (String.IsNullOrWhiteSpace(url))
253 throw new ArgumentNullException(url);
254 if (treeHash == null)
255 throw new ArgumentNullException("treeHash");
256 if (String.IsNullOrWhiteSpace(cloudFile.Container))
257 throw new ArgumentException("Invalid container", "cloudFile");
258 Contract.EndContractBlock();
261 using (StatusNotification.GetNotifier("Uploading {0}", "Finished Uploading {0}", fileInfo.Name))
263 token.ThrowIfCancellationRequested();
264 await UnpauseEvent.WaitAsync();
266 var fullFileName = fileInfo.GetProperCapitalization();
268 var account = cloudFile.Account ?? accountInfo.UserName;
269 var container = cloudFile.Container;
272 var client = new CloudFilesClient(accountInfo);
273 //Send the hashmap to the server
274 var missingHashes = await client.PutHashMap(account, container, url, treeHash);
276 ReportUploadProgress(fileInfo.Name, block++, missingHashes.Count, fileInfo.Length);
277 //If the server returns no missing hashes, we are done
278 while (missingHashes.Count > 0)
281 token.ThrowIfCancellationRequested();
282 await UnpauseEvent.WaitAsync();
284 var buffer = new byte[accountInfo.BlockSize];
285 foreach (var missingHash in missingHashes)
287 token.ThrowIfCancellationRequested();
288 await UnpauseEvent.WaitAsync();
290 //Find the proper block
291 var blockIndex = treeHash.HashDictionary[missingHash];
292 long offset = blockIndex*accountInfo.BlockSize;
294 var read = fileInfo.Read(buffer, offset, accountInfo.BlockSize);
298 //And upload the block
299 await client.PostBlock(account, container, buffer, 0, read);
300 Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex, fullFileName);
302 catch (Exception exc)
304 Log.Error(String.Format("Uploading block {0} of {1}", blockIndex, fullFileName), exc);
306 ReportUploadProgress(fileInfo.Name, block++, missingHashes.Count, fileInfo.Length);
309 token.ThrowIfCancellationRequested();
310 //Repeat until there are no more missing hashes
311 missingHashes = await client.PutHashMap(account, container, url, treeHash);
314 ReportUploadProgress(fileInfo.Name, missingHashes.Count, missingHashes.Count, fileInfo.Length);
318 private void ReportUploadProgress(string fileName, int block, int totalBlocks, long fileSize)
320 StatusNotification.Notify(totalBlocks == 0
321 ? new ProgressNotification(fileName, "Uploading", 1, 1, fileSize)
322 : new ProgressNotification(fileName, "Uploading", block, totalBlocks, fileSize));
326 private bool HandleUploadWebException(CloudAction action, WebException exc)
328 var response = exc.Response as HttpWebResponse;
329 if (response == null)
331 if (response.StatusCode == HttpStatusCode.Unauthorized)
333 Log.Error("Not allowed to upload file", exc);
334 var message = String.Format("Not allowed to uplad file {0}", action.LocalFile.FullName);
335 StatusKeeper.SetFileState(action.LocalFile.FullName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");
336 StatusNotification.NotifyChange(message, TraceLevel.Warning);
343 public Selectives Selectives { get; set; }
345 public AsyncManualResetEvent UnpauseEvent { get; set; }