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 private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
60 public string FilePath { get; private set; }
61 public string RelativePath { get; private set; }
63 public string CachePath { get; private set; }
65 public TreeHash ServerHash { get; private set; }
67 public string TempPath { get; private set; }
71 get { return _blocks.Count>0; }
74 readonly ConcurrentDictionary<int, string> _blocks = new ConcurrentDictionary<int, string>();
75 readonly ConcurrentDictionary<string, string> _orphanBlocks = new ConcurrentDictionary<string, string>();
77 [ContractInvariantMethod]
78 private void Invariants()
80 Contract.Invariant(Path.IsPathRooted(CachePath));
81 Contract.Invariant(Path.IsPathRooted(FilePath));
82 Contract.Invariant(Path.IsPathRooted(TempPath));
83 Contract.Invariant(!Path.IsPathRooted(RelativePath));
84 Contract.Invariant(_blocks!=null);
85 Contract.Invariant(_orphanBlocks!=null);
86 Contract.Invariant(ServerHash!=null);
89 public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash)
91 if (String.IsNullOrWhiteSpace(cachePath))
92 throw new ArgumentNullException("cachePath");
93 if (!Path.IsPathRooted(cachePath))
94 throw new ArgumentException("The cachePath must be rooted", "cachePath");
96 if (string.IsNullOrWhiteSpace(filePath))
97 throw new ArgumentNullException("filePath");
98 if (!Path.IsPathRooted(filePath))
99 throw new ArgumentException("The filePath must be rooted", "filePath");
101 if (string.IsNullOrWhiteSpace(relativePath))
102 throw new ArgumentNullException("relativePath");
103 if (Path.IsPathRooted(relativePath))
104 throw new ArgumentException("The relativePath must NOT be rooted", "relativePath");
106 if (serverHash == null)
107 throw new ArgumentNullException("serverHash");
108 Contract.EndContractBlock();
112 RelativePath=relativePath;
113 ServerHash = serverHash;
114 //The file will be stored in a temporary location while downloading with an extension .download
115 TempPath = Path.Combine(CachePath, RelativePath + ".download");
117 //Need to calculate the directory path because RelativePath may include folders
118 var directoryPath = Path.GetDirectoryName(TempPath);
119 //directoryPath CAN be null if TempPath is a root path
120 if (String.IsNullOrWhiteSpace(directoryPath))
121 throw new ArgumentException("TempPath");
122 //CachePath was absolute so directoryPath is absolute too
123 Contract.Assume(Path.IsPathRooted(directoryPath));
125 if (!Directory.Exists(directoryPath))
126 Directory.CreateDirectory(directoryPath);
128 LoadOrphans(directoryPath);
131 private void LoadOrphans(string directoryPath)
133 if (string.IsNullOrWhiteSpace(directoryPath))
134 throw new ArgumentNullException("directoryPath");
135 if (!Path.IsPathRooted(directoryPath))
136 throw new ArgumentException("The directoryPath must be rooted", "directoryPath");
137 if (ServerHash==null)
138 throw new InvalidOperationException("ServerHash wasn't initialized");
139 Contract.EndContractBlock();
141 var fileNamename = Path.GetFileName(FilePath);
142 var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*");
143 foreach (var orphan in orphans)
145 using (HashAlgorithm hasher = HashAlgorithm.Create(ServerHash.BlockHash))
147 var buffer=File.ReadAllBytes(orphan);
148 //The server truncates nulls before calculating hashes, have to do the same
149 //Find the last non-null byte, starting from the end
150 var lastByteIndex = Array.FindLastIndex(buffer, buffer.Length-1, aByte => aByte != 0);
151 //lastByteIndex may be -1 if the file was empty. We don't want to use that block file
152 if (lastByteIndex >= 0)
154 var binHash = hasher.ComputeHash(buffer, 0, lastByteIndex);
155 var hash = binHash.ToHashString();
156 _orphanBlocks[hash] = orphan;
165 if (String.IsNullOrWhiteSpace(FilePath))
166 throw new InvalidOperationException("FilePath is empty");
167 if (String.IsNullOrWhiteSpace(TempPath))
168 throw new InvalidOperationException("TempPath is empty");
169 Contract.EndContractBlock();
171 //Copy the file to a temporary location. Changes will be made to the
172 //temporary file, then it will replace the original file
173 if (File.Exists(FilePath))
174 File.Copy(FilePath, TempPath, true);
176 //Set the size of the file to the size specified in the treehash
177 //This will also create an empty file if the file doesn't exist
180 SetFileSize(TempPath, ServerHash.Bytes);
182 //Update the temporary file with the data from the blocks
183 using (var stream = File.OpenWrite(TempPath))
185 foreach (var block in _blocks)
187 var blockPath = block.Value;
188 var blockIndex = block.Key;
189 using (var blockStream = File.OpenRead(blockPath))
191 long offset = blockIndex*ServerHash.BlockSize;
192 stream.Seek(offset, SeekOrigin.Begin);
193 blockStream.CopyTo(stream);
202 private void SwapFiles()
204 if (String.IsNullOrWhiteSpace(FilePath))
205 throw new InvalidOperationException("FilePath is empty");
206 if (String.IsNullOrWhiteSpace(TempPath))
207 throw new InvalidOperationException("TempPath is empty");
208 Contract.EndContractBlock();
210 if (File.Exists(FilePath))
211 File.Replace(TempPath, FilePath, null, true);
214 var targetDirectory = Path.GetDirectoryName(FilePath);
215 if (!Directory.Exists(targetDirectory))
216 Directory.CreateDirectory(targetDirectory);
217 File.Move(TempPath, FilePath);
221 private void ClearBlocks()
223 if (Log.IsDebugEnabled)
224 Log.DebugFormat("Clearing blocks for {0}",this.FilePath);
225 //Get all the the block paths, orphan or not
226 var paths= _blocks.Select(pair => pair.Value)
227 .Union(_orphanBlocks.Select(pair => pair.Value));
228 foreach (var filePath in paths)
230 File.Delete(filePath);
233 File.Delete(TempPath);
235 _orphanBlocks.Clear();
238 //Change the file's size, possibly truncating or adding to it
239 private void SetFileSize(string filePath, long fileSize)
241 if (String.IsNullOrWhiteSpace(filePath))
242 throw new ArgumentNullException("filePath");
243 if (!Path.IsPathRooted(filePath))
244 throw new ArgumentException("The filePath must be rooted", "filePath");
246 throw new ArgumentOutOfRangeException("fileSize");
247 Contract.EndContractBlock();
249 using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
251 stream.SetLength(fileSize);
255 /* //Check whether we should copy the local file to a temp path
256 private bool ShouldCopy(string localPath, string tempPath)
258 //No need to copy if there is no file
259 if (!File.Exists(localPath))
262 //If there is no temp file, go ahead and copy
263 if (!File.Exists(tempPath))
266 //If there is a temp file and is newer than the actual file, don't copy
267 var localLastWrite = File.GetLastWriteTime(localPath);
268 var tempLastWrite = File.GetLastWriteTime(tempPath);
270 //This could mean there is an interrupted download in progress
271 return (tempLastWrite < localLastWrite);
275 public bool UseOrphan(int blockIndex, string blockHash)
277 string blockPath=null;
278 if (_orphanBlocks.TryGetValue(blockHash,out blockPath))
280 _blocks[blockIndex] = blockPath;
286 public Task StoreBlock(int blockIndex,byte[] buffer)
288 var blockPath = String.Format("{0}.{1:000000}", TempPath, blockIndex);
289 _blocks[blockIndex] = blockPath;
290 //Remove any orphan files
291 if (File.Exists(blockPath))
292 File.Delete(blockPath);
294 return FileAsync.WriteAllBytes(blockPath, buffer);