2 /* -----------------------------------------------------------------------
3 * <copyright file="BlockUpdater.cs" company="GRNet">
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
11 * 1. Redistributions of source code must retain the above
12 * copyright notice, this list of conditions and the following
15 * 2. Redistributions in binary form must reproduce the above
16 * copyright notice, this list of conditions and the following
17 * disclaimer in the documentation and/or other materials
18 * provided with the distribution.
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
34 * The views and conclusions contained in the software and
35 * documentation are those of the authors and should not be
36 * interpreted as representing official policies, either expressed
37 * or implied, of GRNET S.A.
39 * -----------------------------------------------------------------------
43 using System.Collections.Concurrent;
44 using System.Collections.Generic;
45 using System.Diagnostics.Contracts;
48 using System.Reflection;
49 using System.Security.Cryptography;
51 using System.Threading.Tasks;
54 namespace Pithos.Core.Agents
58 //TODO: Must clean orphaned blocks from the Cache folder.
60 //The Cache folder may have orphaned blocks. Blocks may be left in the Cache folder because:
61 //1. A download was in progress when the application terminated. These blocks are needed to proceed
62 // with partial download
63 //2. The application terminated abnormally before the blocks were cleared after a download
64 //3. The server file was deleted before the download completed.
66 //In #1, we need to keep the blocks. We need to detect the other cases and delete orphans
69 // - Delete blocks with no corresponding state
70 // - Check and delete possible orphans when a Deletion is detected
71 // - Add Advanced command "Clear Cache"
73 //Need a better way to differentiate between cases #2, #3 and #1
75 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
77 public string FilePath { get; private set; }
78 public string RelativePath { get; private set; }
80 public string CachePath { get; private set; }
82 public TreeHash ServerHash { get; private set; }
84 public string TempPath { get; private set; }
88 get { return _blocks.Count>0; }
91 readonly ConcurrentDictionary<int, string> _blocks = new ConcurrentDictionary<int, string>();
92 readonly ConcurrentDictionary<string, string> _orphanBlocks = new ConcurrentDictionary<string, string>();
94 [ContractInvariantMethod]
95 private void Invariants()
97 Contract.Invariant(Path.IsPathRooted(CachePath));
98 Contract.Invariant(Path.IsPathRooted(FilePath));
99 Contract.Invariant(Path.IsPathRooted(TempPath));
100 Contract.Invariant(!Path.IsPathRooted(RelativePath));
101 Contract.Invariant(_blocks!=null);
102 Contract.Invariant(_orphanBlocks!=null);
103 Contract.Invariant(ServerHash!=null);
106 public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash)
108 if (String.IsNullOrWhiteSpace(cachePath))
109 throw new ArgumentNullException("cachePath");
110 if (!Path.IsPathRooted(cachePath))
111 throw new ArgumentException("The cachePath must be rooted", "cachePath");
113 if (string.IsNullOrWhiteSpace(filePath))
114 throw new ArgumentNullException("filePath");
115 if (!Path.IsPathRooted(filePath))
116 throw new ArgumentException("The filePath must be rooted", "filePath");
118 if (string.IsNullOrWhiteSpace(relativePath))
119 throw new ArgumentNullException("relativePath");
120 if (Path.IsPathRooted(relativePath))
121 throw new ArgumentException("The relativePath must NOT be rooted", "relativePath");
123 if (serverHash == null)
124 throw new ArgumentNullException("serverHash");
125 Contract.EndContractBlock();
129 RelativePath=relativePath;
130 ServerHash = serverHash;
131 //The file will be stored in a temporary location while downloading with an extension .download
132 TempPath = Path.Combine(CachePath, RelativePath + ".download");
134 //Need to calculate the directory path because RelativePath may include folders
135 var directoryPath = Path.GetDirectoryName(TempPath);
136 //directoryPath CAN be null if TempPath is a root path
137 if (String.IsNullOrWhiteSpace(directoryPath))
138 throw new ArgumentException("TempPath");
139 //CachePath was absolute so directoryPath is absolute too
140 Contract.Assume(Path.IsPathRooted(directoryPath));
142 if (!Directory.Exists(directoryPath))
143 Directory.CreateDirectory(directoryPath);
145 LoadOrphans(directoryPath);
148 private void LoadOrphans(string directoryPath)
150 if (string.IsNullOrWhiteSpace(directoryPath))
151 throw new ArgumentNullException("directoryPath");
152 if (!Path.IsPathRooted(directoryPath))
153 throw new ArgumentException("The directoryPath must be rooted", "directoryPath");
154 if (ServerHash==null)
155 throw new InvalidOperationException("ServerHash wasn't initialized");
156 Contract.EndContractBlock();
158 var fileNamename = Path.GetFileName(FilePath);
159 var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*");
160 foreach (var orphan in orphans)
162 using (HashAlgorithm hasher = HashAlgorithm.Create(ServerHash.BlockHash))
164 var buffer=File.ReadAllBytes(orphan);
165 //The server truncates nulls before calculating hashes, have to do the same
166 //Find the last non-null byte, starting from the end
167 var lastByteIndex = Array.FindLastIndex(buffer, buffer.Length-1, aByte => aByte != 0);
168 //lastByteIndex may be -1 if the file was empty. We don't want to use that block file
169 if (lastByteIndex >= 0)
171 var binHash = hasher.ComputeHash(buffer, 0, lastByteIndex);
172 var hash = binHash.ToHashString();
173 _orphanBlocks[hash] = orphan;
182 if (String.IsNullOrWhiteSpace(FilePath))
183 throw new InvalidOperationException("FilePath is empty");
184 if (String.IsNullOrWhiteSpace(TempPath))
185 throw new InvalidOperationException("TempPath is empty");
186 Contract.EndContractBlock();
188 //Copy the file to a temporary location. Changes will be made to the
189 //temporary file, then it will replace the original file
190 if (File.Exists(FilePath))
191 File.Copy(FilePath, TempPath, true);
193 //Set the size of the file to the size specified in the treehash
194 //This will also create an empty file if the file doesn't exist
197 SetFileSize(TempPath, ServerHash.Bytes);
199 //Update the temporary file with the data from the blocks
200 using (var stream = File.OpenWrite(TempPath))
202 foreach (var block in _blocks)
204 var blockPath = block.Value;
205 var blockIndex = block.Key;
206 using (var blockStream = File.OpenRead(blockPath))
208 long offset = blockIndex*ServerHash.BlockSize;
209 stream.Seek(offset, SeekOrigin.Begin);
210 blockStream.CopyTo(stream);
219 private void SwapFiles()
221 if (String.IsNullOrWhiteSpace(FilePath))
222 throw new InvalidOperationException("FilePath is empty");
223 if (String.IsNullOrWhiteSpace(TempPath))
224 throw new InvalidOperationException("TempPath is empty");
225 Contract.EndContractBlock();
227 if (File.Exists(FilePath))
228 File.Replace(TempPath, FilePath, null, true);
231 var targetDirectory = Path.GetDirectoryName(FilePath);
232 if (!Directory.Exists(targetDirectory))
233 Directory.CreateDirectory(targetDirectory);
234 File.Move(TempPath, FilePath);
238 private void ClearBlocks()
240 if (Log.IsDebugEnabled)
241 Log.DebugFormat("Clearing blocks for {0}",this.FilePath);
242 //Get all the the block paths, orphan or not
243 var paths= _blocks.Select(pair => pair.Value)
244 .Union(_orphanBlocks.Select(pair => pair.Value));
245 foreach (var filePath in paths)
247 File.Delete(filePath);
250 File.Delete(TempPath);
252 _orphanBlocks.Clear();
255 //Change the file's size, possibly truncating or adding to it
256 private void SetFileSize(string filePath, long fileSize)
258 if (String.IsNullOrWhiteSpace(filePath))
259 throw new ArgumentNullException("filePath");
260 if (!Path.IsPathRooted(filePath))
261 throw new ArgumentException("The filePath must be rooted", "filePath");
263 throw new ArgumentOutOfRangeException("fileSize");
264 Contract.EndContractBlock();
266 using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
268 stream.SetLength(fileSize);
272 /* //Check whether we should copy the local file to a temp path
273 private bool ShouldCopy(string localPath, string tempPath)
275 //No need to copy if there is no file
276 if (!File.Exists(localPath))
279 //If there is no temp file, go ahead and copy
280 if (!File.Exists(tempPath))
283 //If there is a temp file and is newer than the actual file, don't copy
284 var localLastWrite = File.GetLastWriteTime(localPath);
285 var tempLastWrite = File.GetLastWriteTime(tempPath);
287 //This could mean there is an interrupted download in progress
288 return (tempLastWrite < localLastWrite);
292 public bool UseOrphan(int blockIndex, string blockHash)
294 string blockPath=null;
295 if (_orphanBlocks.TryGetValue(blockHash,out blockPath))
297 _blocks[blockIndex] = blockPath;
303 public Task StoreBlock(int blockIndex,byte[] buffer)
305 var blockPath = String.Format("{0}.{1:000000}", TempPath, blockIndex);
306 _blocks[blockIndex] = blockPath;
307 //Remove any orphan files
308 if (File.Exists(blockPath))
309 File.Delete(blockPath);
311 return FileAsync.WriteAllBytes(blockPath, buffer);