2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="BlockUpdater.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\r
7 * Redistribution and use in source and binary forms, with or
\r
8 * without modification, are permitted provided that the following
\r
9 * conditions are met:
\r
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\r
15 * 2. Redistributions in binary form must reproduce the above
\r
16 * copyright notice, this list of conditions and the following
\r
17 * disclaimer in the documentation and/or other materials
\r
18 * provided with the distribution.
\r
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
\r
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
\r
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
\r
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
\r
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
\r
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
\r
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
\r
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
\r
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
\r
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
\r
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
\r
32 * POSSIBILITY OF SUCH DAMAGE.
\r
34 * The views and conclusions contained in the software and
\r
35 * documentation are those of the authors and should not be
\r
36 * interpreted as representing official policies, either expressed
\r
37 * or implied, of GRNET S.A.
\r
39 * -----------------------------------------------------------------------
\r
43 using System.Collections.Concurrent;
\r
44 using System.Diagnostics.Contracts;
\r
47 using System.Reflection;
\r
48 using System.Threading;
\r
49 using System.Threading.Tasks;
\r
50 using OpenSSL.Crypto;
\r
51 using Pithos.Network;
\r
52 using Alpha = Alphaleonis.Win32.Filesystem;
\r
54 namespace Pithos.Core.Agents
\r
58 //TODO: Must clean orphaned blocks from the Cache folder.
\r
60 //The Cache folder may have orphaned blocks. Blocks may be left in the Cache folder because:
\r
61 //1. A download was in progress when the application terminated. These blocks are needed to proceed
\r
62 // with partial download
\r
63 //2. The application terminated abnormally before the blocks were cleared after a download
\r
64 //3. The server file was deleted before the download completed.
\r
66 //In #1, we need to keep the blocks. We need to detect the other cases and delete orphans
\r
69 // - Delete blocks with no corresponding state
\r
70 // - Check and delete possible orphans when a Deletion is detected
\r
71 // - Add Advanced command "Clear Cache"
\r
73 //Need a better way to differentiate between cases #2, #3 and #1
\r
75 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
77 public string FilePath { get; private set; }
\r
78 public string RelativePath { get; private set; }
\r
80 public string CachePath { get; private set; }
\r
82 public TreeHash ServerHash { get; private set; }
\r
84 public string TempPath { get; private set; }
\r
86 public bool HasBlocks
\r
88 get { return _blocks.Count>0; }
\r
91 readonly ConcurrentDictionary<long, string> _blocks = new ConcurrentDictionary<long, string>();
\r
92 readonly ConcurrentDictionary<string, string> _orphanBlocks = new ConcurrentDictionary<string, string>();
\r
94 [ContractInvariantMethod]
\r
95 private void Invariants()
\r
97 Contract.Invariant(Path.IsPathRooted(CachePath));
\r
98 Contract.Invariant(Path.IsPathRooted(FilePath));
\r
99 Contract.Invariant(Path.IsPathRooted(TempPath));
\r
100 Contract.Invariant(!Path.IsPathRooted(RelativePath));
\r
101 Contract.Invariant(_blocks!=null);
\r
102 Contract.Invariant(_orphanBlocks!=null);
\r
103 Contract.Invariant(ServerHash!=null);
\r
106 public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash)
\r
108 if (String.IsNullOrWhiteSpace(cachePath))
\r
109 throw new ArgumentNullException("cachePath");
\r
110 if (!Path.IsPathRooted(cachePath))
\r
111 throw new ArgumentException("The cachePath must be rooted", "cachePath");
\r
113 if (string.IsNullOrWhiteSpace(filePath))
\r
114 throw new ArgumentNullException("filePath");
\r
115 if (!Path.IsPathRooted(filePath))
\r
116 throw new ArgumentException("The filePath must be rooted", "filePath");
\r
118 if (string.IsNullOrWhiteSpace(relativePath))
\r
119 throw new ArgumentNullException("relativePath");
\r
120 if (Path.IsPathRooted(relativePath))
\r
121 throw new ArgumentException("The relativePath must NOT be rooted", "relativePath");
\r
123 if (serverHash == null)
\r
124 throw new ArgumentNullException("serverHash");
\r
125 Contract.EndContractBlock();
\r
127 CachePath=cachePath;
\r
128 FilePath = filePath;
\r
129 RelativePath=relativePath;
\r
130 ServerHash = serverHash;
\r
131 //The file will be stored in a temporary location while downloading with an extension .download
\r
132 TempPath = Path.Combine(CachePath, RelativePath + ".download");
\r
134 //Need to calculate the directory path because RelativePath may include folders
\r
135 var directoryPath = Path.GetDirectoryName(TempPath);
\r
136 //directoryPath CAN be null if TempPath is a root path
\r
137 if (String.IsNullOrWhiteSpace(directoryPath))
\r
138 throw new ArgumentException("TempPath");
\r
139 //CachePath was absolute so directoryPath is absolute too
\r
140 Contract.Assume(Path.IsPathRooted(directoryPath));
\r
142 if (!Directory.Exists(directoryPath))
\r
143 Directory.CreateDirectory(directoryPath);
\r
145 LoadOrphans(directoryPath);
\r
148 private void LoadOrphans(string directoryPath)
\r
150 if (string.IsNullOrWhiteSpace(directoryPath))
\r
151 throw new ArgumentNullException("directoryPath");
\r
152 if (!Path.IsPathRooted(directoryPath))
\r
153 throw new ArgumentException("The directoryPath must be rooted", "directoryPath");
\r
154 if (ServerHash==null)
\r
155 throw new InvalidOperationException("ServerHash wasn't initialized");
\r
156 Contract.EndContractBlock();
\r
158 var fileNamename = Path.GetFileName(FilePath);
\r
159 var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*");
\r
160 foreach (var orphan in orphans)
\r
162 using (var hasher = new MessageDigestContext(MessageDigest.CreateByName(ServerHash.BlockHash)))
\r
165 var buffer=File.ReadAllBytes(orphan);
\r
166 //The server truncates nulls before calculating hashes, have to do the same
\r
167 //Find the last non-null byte, starting from the end
\r
168 var lastByteIndex = Array.FindLastIndex(buffer, buffer.Length-1, aByte => aByte != 0);
\r
169 //lastByteIndex may be -1 if the file was empty. We don't want to use that block file
\r
170 if (lastByteIndex >= 0)
\r
173 if (lastByteIndex == buffer.Length - 1)
\r
177 block=new byte[lastByteIndex+1];
\r
178 Buffer.BlockCopy(buffer,0,block,0,lastByteIndex+1);
\r
180 var binHash = hasher.Digest(block);
\r
181 var hash = binHash.ToHashString();
\r
182 _orphanBlocks[hash] = orphan;
\r
189 public void Commit()
\r
191 if (String.IsNullOrWhiteSpace(FilePath))
\r
192 throw new InvalidOperationException("FilePath is empty");
\r
193 if (String.IsNullOrWhiteSpace(TempPath))
\r
194 throw new InvalidOperationException("TempPath is empty");
\r
195 Contract.EndContractBlock();
\r
197 var info = Alpha.Volume.GetVolumeInformation(Alpha.Path.GetPathRoot(FilePath));
\r
198 var isNTFS = info.FileSystemName.Equals("NTFS");
\r
201 if (isNTFS && MS.WindowsAPICodePack.Internal.CoreHelpers.RunningOnVista)
\r
203 ApplyBlocksWithTransaction();
\r
207 ApplyBlocksWithCopy();
\r
212 private void ApplyBlocksWithTransaction()
\r
214 var targetFile = FilePath;
\r
215 using (var tx = new Alpha.KernelTransaction())
\r
217 //Retry if the file is still open
\r
218 using (var stream = Alpha.File.Open(tx, targetFile,
\r
219 Alpha.FileMode.OpenOrCreate,Alpha.FileAccess.Write))
\r
221 stream.SetLength(ServerHash.Bytes);
\r
222 foreach (var block in _blocks)
\r
224 var blockPath = block.Value;
\r
225 var blockIndex = block.Key;
\r
226 //Retry if we can't open the block immediatelly
\r
227 for (int i = 0; i < 3; i++)
\r
231 using (var blockStream = File.OpenRead(blockPath))
\r
233 long offset = blockIndex*ServerHash.BlockSize;
\r
234 stream.Seek(offset, SeekOrigin.Begin);
\r
235 blockStream.CopyTo(stream);
\r
239 catch (IOException)
\r
243 Thread.Sleep((i+1)*300);
\r
253 private void ApplyBlocksWithCopy()
\r
255 //Copy the file to a temporary location. Changes will be made to the
\r
256 //temporary file, then it will replace the original file
\r
257 if (File.Exists(FilePath))
\r
258 File.Copy(FilePath, TempPath, true);
\r
260 //Set the size of the file to the size specified in the treehash
\r
261 //This will also create an empty file if the file doesn't exist
\r
263 ApplyBlocks(TempPath);
\r
267 private void ApplyBlocks(string targetFile)
\r
269 SetFileSize(targetFile, ServerHash.Bytes);
\r
271 //Update the temporary file with the data from the blocks
\r
272 using (var stream = File.OpenWrite(targetFile))
\r
274 foreach (var block in _blocks)
\r
276 var blockPath = block.Value;
\r
277 var blockIndex = block.Key;
\r
278 using (var blockStream = File.OpenRead(blockPath))
\r
280 long offset = blockIndex*ServerHash.BlockSize;
\r
281 stream.Seek(offset, SeekOrigin.Begin);
\r
282 blockStream.CopyTo(stream);
\r
288 private void SwapFiles()
\r
290 if (String.IsNullOrWhiteSpace(FilePath))
\r
291 throw new InvalidOperationException("FilePath is empty");
\r
292 if (String.IsNullOrWhiteSpace(TempPath))
\r
293 throw new InvalidOperationException("TempPath is empty");
\r
294 Contract.EndContractBlock();
\r
296 if (File.Exists(FilePath))
\r
297 File.Replace(TempPath, FilePath, null, true);
\r
300 var targetDirectory = Path.GetDirectoryName(FilePath);
\r
301 if (!Directory.Exists(targetDirectory))
\r
302 Directory.CreateDirectory(targetDirectory);
\r
303 File.Move(TempPath, FilePath);
\r
307 private void ClearBlocks()
\r
309 if (Log.IsDebugEnabled)
\r
310 Log.DebugFormat("Clearing blocks for {0}",this.FilePath);
\r
311 //Get all the the block paths, orphan or not
\r
312 var paths= _blocks.Select(pair => pair.Value)
\r
313 .Union(_orphanBlocks.Select(pair => pair.Value));
\r
314 foreach (var filePath in paths)
\r
316 File.Delete(filePath);
\r
319 File.Delete(TempPath);
\r
321 _orphanBlocks.Clear();
\r
324 //Change the file's size, possibly truncating or adding to it
\r
325 private void SetFileSize(string filePath, long fileSize)
\r
327 if (String.IsNullOrWhiteSpace(filePath))
\r
328 throw new ArgumentNullException("filePath");
\r
329 if (!Path.IsPathRooted(filePath))
\r
330 throw new ArgumentException("The filePath must be rooted", "filePath");
\r
332 throw new ArgumentOutOfRangeException("fileSize");
\r
333 Contract.EndContractBlock();
\r
335 using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
\r
337 stream.SetLength(fileSize);
\r
341 /* //Check whether we should copy the local file to a temp path
\r
342 private bool ShouldCopy(string localPath, string tempPath)
\r
344 //No need to copy if there is no file
\r
345 if (!File.Exists(localPath))
\r
348 //If there is no temp file, go ahead and copy
\r
349 if (!File.Exists(tempPath))
\r
352 //If there is a temp file and is newer than the actual file, don't copy
\r
353 var localLastWrite = File.GetLastWriteTime(localPath);
\r
354 var tempLastWrite = File.GetLastWriteTime(tempPath);
\r
356 //This could mean there is an interrupted download in progress
\r
357 return (tempLastWrite < localLastWrite);
\r
361 public bool UseOrphan(long blockIndex, string blockHash)
\r
363 string blockPath=null;
\r
364 if (_orphanBlocks.TryGetValue(blockHash,out blockPath))
\r
366 _blocks[blockIndex] = blockPath;
\r
372 public Task StoreBlock(long blockIndex,byte[] buffer)
\r
374 var blockPath = String.Format("{0}.{1:000000}", TempPath, blockIndex);
\r
375 _blocks[blockIndex] = blockPath;
\r
376 //Remove any orphan files
\r
377 if (File.Exists(blockPath))
\r
378 File.Delete(blockPath);
\r
380 return FileAsync.WriteAllBytes(blockPath, buffer);
\r