2 using System.ComponentModel.Composition;
3 using System.Diagnostics.Contracts;
5 using System.Reflection;
6 using System.Threading.Tasks;
7 using Pithos.Interfaces;
11 namespace Pithos.Core.Agents
13 [Export(typeof(Downloader))]
14 public class Downloader
16 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
19 private IStatusKeeper StatusKeeper { get; set; }
22 public IStatusNotification StatusNotification { get; set; }
25 public async Task DownloadCloudFile(AccountInfo accountInfo, ObjectInfo cloudFile, string filePath)
27 if (accountInfo == null)
28 throw new ArgumentNullException("accountInfo");
29 if (cloudFile == null)
30 throw new ArgumentNullException("cloudFile");
31 if (String.IsNullOrWhiteSpace(cloudFile.Account))
32 throw new ArgumentNullException("cloudFile");
33 if (String.IsNullOrWhiteSpace(cloudFile.Container))
34 throw new ArgumentNullException("cloudFile");
35 if (String.IsNullOrWhiteSpace(filePath))
36 throw new ArgumentNullException("filePath");
37 if (!Path.IsPathRooted(filePath))
38 throw new ArgumentException("The filePath must be rooted", "filePath");
39 Contract.EndContractBlock();
41 using (ThreadContext.Stacks["Operation"].Push("DownloadCloudFile"))
44 var localPath = FileInfoExtensions.GetProperFilePathCapitalization(filePath);
45 var relativeUrl = new Uri(cloudFile.Name, UriKind.Relative);
47 var url = relativeUrl.ToString();
48 if (cloudFile.Name.EndsWith(".ignore", StringComparison.InvariantCultureIgnoreCase))
52 //Are we already downloading or uploading the file?
53 using (var gate = NetworkGate.Acquire(localPath, NetworkOperation.Downloading))
58 var client = new CloudFilesClient(accountInfo);
59 var account = cloudFile.Account;
60 var container = cloudFile.Container;
62 if (cloudFile.IsDirectory)
64 if (!Directory.Exists(localPath))
67 Directory.CreateDirectory(localPath);
68 if (Log.IsDebugEnabled)
69 Log.DebugFormat("Created Directory [{0}]", localPath);
73 var localInfo = new FileInfo(localPath);
74 if (localInfo.Exists && localInfo.Length == 0)
76 Log.WarnFormat("Malformed directory object detected for [{0}]", localPath);
78 Directory.CreateDirectory(localPath);
79 if (Log.IsDebugEnabled)
80 Log.DebugFormat("Created Directory [{0}]", localPath);
86 var isChanged = IsObjectChanged(cloudFile, localPath);
89 //Retrieve the hashmap from the server
90 var serverHash = await client.GetHashMap(account, container, url);
91 //If it's a small file
92 if (serverHash.Hashes.Count == 1)
93 //Download it in one go
95 DownloadEntireFileAsync(accountInfo, client, cloudFile, relativeUrl, localPath);
96 //Otherwise download it block by block
99 DownloadWithBlocks(accountInfo, client, cloudFile, relativeUrl, localPath,
102 if (!cloudFile.IsWritable(accountInfo.UserName))
104 var attributes = File.GetAttributes(localPath);
105 File.SetAttributes(localPath, attributes | FileAttributes.ReadOnly);
110 //Now we can store the object's metadata without worrying about ghost status entries
111 StatusKeeper.StoreInfo(localPath, cloudFile);
117 //Download a file asynchronously using blocks
118 public async Task DownloadWithBlocks(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath, TreeHash serverHash)
121 throw new ArgumentNullException("client");
122 if (cloudFile == null)
123 throw new ArgumentNullException("cloudFile");
124 if (relativeUrl == null)
125 throw new ArgumentNullException("relativeUrl");
126 if (String.IsNullOrWhiteSpace(filePath))
127 throw new ArgumentNullException("filePath");
128 if (!Path.IsPathRooted(filePath))
129 throw new ArgumentException("The filePath must be rooted", "filePath");
130 if (serverHash == null)
131 throw new ArgumentNullException("serverHash");
132 if (cloudFile.IsDirectory)
133 throw new ArgumentException("cloudFile is a directory, not a file", "cloudFile");
134 Contract.EndContractBlock();
136 var fileAgent = GetFileAgent(accountInfo);
137 var localPath = FileInfoExtensions.GetProperFilePathCapitalization(filePath);
139 //Calculate the relative file path for the new file
140 var relativePath = relativeUrl.RelativeUriToFilePath();
141 var blockUpdater = new BlockUpdater(fileAgent.CachePath, localPath, relativePath, serverHash);
144 StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing, String.Format("Calculating hashmap for {0} before download", Path.GetFileName(localPath)));
145 //Calculate the file's treehash
146 var treeHash = await Signature.CalculateTreeHashAsync(localPath, serverHash.BlockSize, serverHash.BlockHash, 2);
148 //And compare it with the server's hash
149 var upHashes = serverHash.GetHashesAsStrings();
150 var localHashes = treeHash.HashDictionary;
151 ReportDownloadProgress(Path.GetFileName(localPath), 0, upHashes.Length, cloudFile.Bytes);
152 for (int i = 0; i < upHashes.Length; i++)
154 //For every non-matching hash
155 var upHash = upHashes[i];
156 if (!localHashes.ContainsKey(upHash))
158 StatusNotification.Notify(new CloudNotification { Data = cloudFile });
160 if (blockUpdater.UseOrphan(i, upHash))
162 Log.InfoFormat("[BLOCK GET] ORPHAN FOUND for {0} of {1} for {2}", i, upHashes.Length, localPath);
165 Log.InfoFormat("[BLOCK GET] START {0} of {1} for {2}", i, upHashes.Length, localPath);
166 var start = i * serverHash.BlockSize;
167 //To download the last block just pass a null for the end of the range
169 if (i < upHashes.Length - 1)
170 end = ((i + 1) * serverHash.BlockSize);
172 //Download the missing block
173 var block = await client.GetBlock(cloudFile.Account, cloudFile.Container, relativeUrl, start, end);
176 blockUpdater.StoreBlock(i, block);
179 Log.InfoFormat("[BLOCK GET] FINISH {0} of {1} for {2}", i, upHashes.Length, localPath);
181 ReportDownloadProgress(Path.GetFileName(localPath), i, upHashes.Length, cloudFile.Bytes);
184 //Want to avoid notifications if no changes were made
185 var hasChanges = blockUpdater.HasBlocks;
186 blockUpdater.Commit();
189 //Notify listeners that a local file has changed
190 StatusNotification.NotifyChangedFile(localPath);
192 Log.InfoFormat("[BLOCK GET] COMPLETE {0}", localPath);
195 //Download a small file with a single GET operation
196 private async Task DownloadEntireFileAsync(AccountInfo accountInfo, CloudFilesClient client, ObjectInfo cloudFile, Uri relativeUrl, string filePath)
199 throw new ArgumentNullException("client");
200 if (cloudFile == null)
201 throw new ArgumentNullException("cloudFile");
202 if (relativeUrl == null)
203 throw new ArgumentNullException("relativeUrl");
204 if (String.IsNullOrWhiteSpace(filePath))
205 throw new ArgumentNullException("filePath");
206 if (!Path.IsPathRooted(filePath))
207 throw new ArgumentException("The localPath must be rooted", "filePath");
208 if (cloudFile.IsDirectory)
209 throw new ArgumentException("cloudFile is a directory, not a file", "cloudFile");
210 Contract.EndContractBlock();
212 var localPath = FileInfoExtensions.GetProperFilePathCapitalization(filePath);
213 StatusNotification.SetPithosStatus(PithosStatus.LocalSyncing, String.Format("Downloading {0}", Path.GetFileName(localPath)));
214 StatusNotification.Notify(new CloudNotification { Data = cloudFile });
216 var fileAgent = GetFileAgent(accountInfo);
217 //Calculate the relative file path for the new file
218 var relativePath = relativeUrl.RelativeUriToFilePath();
219 //The file will be stored in a temporary location while downloading with an extension .download
220 var tempPath = Path.Combine(fileAgent.CachePath, relativePath + ".download");
221 //Make sure the target folder exists. DownloadFileTask will not create the folder
222 var tempFolder = Path.GetDirectoryName(tempPath);
223 if (!Directory.Exists(tempFolder))
224 Directory.CreateDirectory(tempFolder);
226 //Download the object to the temporary location
227 await client.GetObject(cloudFile.Account, cloudFile.Container, relativeUrl.ToString(), tempPath);
229 //Create the local folder if it doesn't exist (necessary for shared objects)
230 var localFolder = Path.GetDirectoryName(localPath);
231 if (!Directory.Exists(localFolder))
234 Directory.CreateDirectory(localFolder);
238 //A file may already exist that has the same name as the new folder.
239 //This may be an artifact of the way Pithos handles directories
240 var fileInfo = new FileInfo(localFolder);
241 if (fileInfo.Exists && fileInfo.Length == 0)
243 Log.WarnFormat("Malformed directory object detected for [{0}]", localFolder);
245 Directory.CreateDirectory(localFolder);
250 //And move it to its actual location once downloading is finished
251 if (File.Exists(localPath))
252 File.Replace(tempPath, localPath, null, true);
254 File.Move(tempPath, localPath);
255 //Notify listeners that a local file has changed
256 StatusNotification.NotifyChangedFile(localPath);
262 private void ReportDownloadProgress(string fileName, int block, int totalBlocks, long fileSize)
264 StatusNotification.Notify(totalBlocks == 0
265 ? new ProgressNotification(fileName, "Downloading", 1, 1, fileSize)
266 : new ProgressNotification(fileName, "Downloading", block, totalBlocks, fileSize));
269 private bool IsObjectChanged(ObjectInfo cloudFile, string localPath)
271 //If the target is a directory, there are no changes to download
272 if (Directory.Exists(localPath))
274 //If the file doesn't exist, we have a chagne
275 if (!File.Exists(localPath))
277 //If there is no stored state, we have a change
278 var localState = StatusKeeper.GetStateByFilePath(localPath);
279 if (localState == null)
282 var info = new FileInfo(localPath);
283 var shortHash = info.ComputeShortHash();
284 //If the file is different from the stored state, we have a change
285 if (localState.ShortHash != shortHash)
287 //If the top hashes differ, we have a change
288 return (localState.Checksum != cloudFile.Hash);
291 private static FileAgent GetFileAgent(AccountInfo accountInfo)
293 return AgentLocator<FileAgent>.Get(accountInfo.AccountPath);