2 using System.Collections.Generic;
\r
3 using System.ComponentModel.Composition;
\r
4 using System.Diagnostics;
\r
5 using System.Diagnostics.Contracts;
\r
9 using System.Reflection;
\r
10 using System.Security.Cryptography;
\r
11 using System.Threading;
\r
12 using System.Threading.Tasks;
\r
13 using Pithos.Interfaces;
\r
14 using Pithos.Network;
\r
17 namespace Pithos.Core.Agents
\r
19 [Export(typeof(Uploader))]
\r
20 public class Uploader
\r
22 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
25 private IStatusKeeper StatusKeeper { get; set; }
\r
28 public IStatusNotification StatusNotification { get; set; }
\r
31 //CancellationTokenSource _cts = new CancellationTokenSource();
\r
32 /*public void SignalStop()
\r
37 public async Task UploadCloudFile(CloudAction action,CancellationToken cancellationToken)
\r
40 throw new ArgumentNullException("action");
\r
41 Contract.EndContractBlock();
\r
43 using (ThreadContext.Stacks["Operation"].Push("UploadCloudFile"))
\r
47 await UnpauseEvent.WaitAsync();
\r
49 var fileInfo = action.LocalFile;
\r
51 if (fileInfo.Extension.Equals("ignore", StringComparison.InvariantCultureIgnoreCase))
\r
54 if (!Selectives.IsSelected(action.AccountInfo, fileInfo))
\r
57 //Try to load the action's local state, if it is empty
\r
58 if (action.FileState == null)
\r
59 action.FileState = StatusKeeper.GetStateByFilePath(fileInfo.FullName);
\r
60 if (action.FileState == null)
\r
62 Log.WarnFormat("File [{0}] has no local state. It was probably created by a download action", fileInfo.FullName);
\r
66 var latestState = action.FileState;
\r
68 //Do not upload files in conflict
\r
69 if (latestState.FileStatus == FileStatus.Conflict)
\r
71 Log.InfoFormat("Skipping file in conflict [{0}]", fileInfo.FullName);
\r
74 //Do not upload files when we have no permission
\r
75 if (latestState.FileStatus == FileStatus.Forbidden)
\r
77 Log.InfoFormat("Skipping forbidden file [{0}]", fileInfo.FullName);
\r
81 //Are we targeting our own account or a sharer account?
\r
82 var relativePath = fileInfo.AsRelativeTo(action.AccountInfo.AccountPath);
\r
83 var accountInfo = relativePath.StartsWith(FolderConstants.OthersFolder)
\r
84 ? GetSharerAccount(relativePath, action.AccountInfo)
\r
85 : action.AccountInfo;
\r
89 var fullFileName = fileInfo.GetProperCapitalization();
\r
90 using (var gate = NetworkGate.Acquire(fullFileName, NetworkOperation.Uploading))
\r
92 //Abort if the file is already being uploaded or downloaded
\r
96 var cloudFile = action.CloudFile;
\r
97 var account = cloudFile.Account ?? accountInfo.UserName;
\r
101 var client = new CloudFilesClient(accountInfo);
\r
103 //Even if GetObjectInfo times out, we can proceed with the upload
\r
104 var cloudInfo = client.GetObjectInfo(account, cloudFile.Container, cloudFile.Name);
\r
106 //If this a shared file
\r
107 if (!cloudFile.Account.Equals(action.AccountInfo.UserName,StringComparison.InvariantCultureIgnoreCase))
\r
111 if (!cloudInfo.IsWritable(action.AccountInfo.UserName))
\r
113 MakeFileReadOnly(fullFileName);
\r
114 StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");
\r
119 //If this is a read-only file, do not upload changes
\r
120 if ( !cloudInfo.IsWritable(action.AccountInfo.UserName) ||
\r
121 //If the file is new, but we can't upload it
\r
122 (!cloudInfo.Exists && !client.CanUpload(account, cloudFile)) )
\r
124 MakeFileReadOnly(fullFileName);
\r
125 StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");
\r
131 await UnpauseEvent.WaitAsync();
\r
133 if (fileInfo is DirectoryInfo)
\r
135 //If the directory doesn't exist the Hash property will be empty
\r
136 if (String.IsNullOrWhiteSpace(cloudInfo.Hash))
\r
137 //Go on and create the directory
\r
138 await client.PutObject(account, cloudFile.Container, cloudFile.Name, fullFileName,
\r
139 String.Empty, "application/directory");
\r
144 var cloudHash = cloudInfo.Hash.ToLower();
\r
148 using(StatusNotification.GetNotifier("Hashing {0} for Upload", "Finished hashing {0}",fileInfo.Name))
\r
150 treeHash = action.TreeHash.Value;
\r
151 topHash = treeHash.TopHash.ToHashString();
\r
156 //If the file hashes match, abort the upload
\r
157 if (cloudInfo != ObjectInfo.Empty && (topHash == cloudHash ))
\r
159 //but store any metadata changes
\r
160 StatusKeeper.StoreInfo(fullFileName, cloudInfo);
\r
161 Log.InfoFormat("Skip upload of {0}, hashes match", fullFileName);
\r
166 //Mark the file as modified while we upload it
\r
167 StatusKeeper.SetFileOverlayStatus(fullFileName, FileOverlayStatus.Modified);
\r
168 //And then upload it
\r
170 //Upload even small files using the Hashmap. The server may already contain
\r
171 //the relevant block
\r
175 await UploadWithHashMap(accountInfo, cloudFile, fileInfo as FileInfo, cloudFile.Name, treeHash,cancellationToken);
\r
177 //If everything succeeds, change the file and overlay status to normal
\r
178 StatusKeeper.SetFileState(fullFileName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");
\r
180 catch (WebException exc)
\r
182 var response = (exc.Response as HttpWebResponse);
\r
183 if (response == null)
\r
185 if (response.StatusCode == HttpStatusCode.Forbidden)
\r
187 StatusKeeper.SetFileState(fileInfo.FullName, FileStatus.Forbidden, FileOverlayStatus.Conflict, "Forbidden");
\r
188 MakeFileReadOnly(fullFileName);
\r
191 //In any other case, propagate the error
\r
195 //Notify the Shell to update the overlays
\r
196 NativeMethods.RaiseChangeNotification(fullFileName);
\r
197 StatusNotification.NotifyChangedFile(fullFileName);
\r
199 catch (AggregateException ex)
\r
201 var exc = ex.InnerException as WebException;
\r
203 throw ex.InnerException;
\r
204 if (HandleUploadWebException(action, exc))
\r
208 catch (WebException ex)
\r
210 if (HandleUploadWebException(action, ex))
\r
214 catch (Exception ex)
\r
216 Log.Error("Unexpected error while uploading file", ex);
\r
222 private static void MakeFileReadOnly(string fullFileName)
\r
224 var attributes = File.GetAttributes(fullFileName);
\r
225 //Do not make any modifications if not necessary
\r
226 if (attributes.HasFlag(FileAttributes.ReadOnly))
\r
228 File.SetAttributes(fullFileName, attributes | FileAttributes.ReadOnly);
\r
231 private static AccountInfo GetSharerAccount(string relativePath, AccountInfo accountInfo)
\r
233 var parts = relativePath.Split('\\');
\r
234 var accountName = parts[1];
\r
235 var oldName = accountInfo.UserName;
\r
236 var absoluteUri = accountInfo.StorageUri.AbsoluteUri;
\r
237 var nameIndex = absoluteUri.IndexOf(oldName, StringComparison.Ordinal);
\r
238 var root = absoluteUri.Substring(0, nameIndex);
\r
240 accountInfo = new AccountInfo
\r
242 UserName = accountName,
\r
243 AccountPath = Path.Combine(accountInfo.AccountPath, parts[0], parts[1]),
\r
244 StorageUri = new Uri(root + accountName),
\r
245 BlockHash = accountInfo.BlockHash,
\r
246 BlockSize = accountInfo.BlockSize,
\r
247 Token = accountInfo.Token
\r
249 return accountInfo;
\r
253 public async Task UploadWithHashMap(AccountInfo accountInfo, ObjectInfo cloudFile, FileInfo fileInfo, string url, TreeHash treeHash, CancellationToken token)
\r
255 if (accountInfo == null)
\r
256 throw new ArgumentNullException("accountInfo");
\r
257 if (cloudFile == null)
\r
258 throw new ArgumentNullException("cloudFile");
\r
259 if (fileInfo == null)
\r
260 throw new ArgumentNullException("fileInfo");
\r
261 if (String.IsNullOrWhiteSpace(url))
\r
262 throw new ArgumentNullException(url);
\r
263 if (treeHash == null)
\r
264 throw new ArgumentNullException("treeHash");
\r
265 if (String.IsNullOrWhiteSpace(cloudFile.Container))
\r
266 throw new ArgumentException("Invalid container", "cloudFile");
\r
267 Contract.EndContractBlock();
\r
270 using (StatusNotification.GetNotifier("Uploading {0}", "Finished Uploading {0}", fileInfo.Name))
\r
272 if (await WaitOrAbort(accountInfo,cloudFile, token))
\r
275 var fullFileName = fileInfo.GetProperCapitalization();
\r
277 var account = cloudFile.Account ?? accountInfo.UserName;
\r
278 var container = cloudFile.Container;
\r
281 var client = new CloudFilesClient(accountInfo);
\r
282 //Send the hashmap to the server
\r
283 var missingHashes = await client.PutHashMap(account, container, url, treeHash);
\r
285 ReportUploadProgress(fileInfo.Name, block++, missingHashes.Count, fileInfo.Length);
\r
286 //If the server returns no missing hashes, we are done
\r
287 while (missingHashes.Count > 0)
\r
290 if (await WaitOrAbort(accountInfo,cloudFile, token))
\r
294 var buffer = new byte[accountInfo.BlockSize];
\r
295 foreach (var missingHash in missingHashes)
\r
297 if (await WaitOrAbort(accountInfo,cloudFile, token))
\r
301 //Find the proper block
\r
302 var blockIndex = treeHash.HashDictionary[missingHash];
\r
303 long offset = blockIndex*accountInfo.BlockSize;
\r
305 var read = fileInfo.Read(buffer, offset, accountInfo.BlockSize);
\r
309 //And upload the block
\r
310 await client.PostBlock(account, container, buffer, 0, read);
\r
311 Log.InfoFormat("[BLOCK] Block {0} of {1} uploaded", blockIndex, fullFileName);
\r
313 catch (Exception exc)
\r
315 Log.Error(String.Format("Uploading block {0} of {1}", blockIndex, fullFileName), exc);
\r
317 ReportUploadProgress(fileInfo.Name, block++, missingHashes.Count, fileInfo.Length);
\r
320 token.ThrowIfCancellationRequested();
\r
321 //Repeat until there are no more missing hashes
\r
322 missingHashes = await client.PutHashMap(account, container, url, treeHash);
\r
325 ReportUploadProgress(fileInfo.Name, missingHashes.Count, missingHashes.Count, fileInfo.Length);
\r
329 private async Task<bool> WaitOrAbort(AccountInfo account,ObjectInfo cloudFile, CancellationToken token)
\r
331 token.ThrowIfCancellationRequested();
\r
332 await UnpauseEvent.WaitAsync();
\r
333 var shouldAbort = !Selectives.IsSelected(account,cloudFile);
\r
335 Log.InfoFormat("Aborting [{0}]",cloudFile.Uri);
\r
336 return shouldAbort;
\r
339 private void ReportUploadProgress(string fileName, int block, int totalBlocks, long fileSize)
\r
341 StatusNotification.Notify(totalBlocks == 0
\r
342 ? new ProgressNotification(fileName, "Uploading", 1, 1, fileSize)
\r
343 : new ProgressNotification(fileName, "Uploading", block, totalBlocks, fileSize));
\r
347 private bool HandleUploadWebException(CloudAction action, WebException exc)
\r
349 var response = exc.Response as HttpWebResponse;
\r
350 if (response == null)
\r
352 if (response.StatusCode == HttpStatusCode.Unauthorized)
\r
354 Log.Error("Not allowed to upload file", exc);
\r
355 var message = String.Format("Not allowed to uplad file {0}", action.LocalFile.FullName);
\r
356 StatusKeeper.SetFileState(action.LocalFile.FullName, FileStatus.Unchanged, FileOverlayStatus.Normal, "");
\r
357 StatusNotification.NotifyChange(message, TraceLevel.Warning);
\r
364 public Selectives Selectives { get; set; }
\r
366 public AsyncManualResetEvent UnpauseEvent { get; set; }
\r