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.Security.Cryptography;
50 using System.Threading.Tasks;
53 namespace Pithos.Core.Agents
57 public string FilePath { get; private set; }
58 public string RelativePath { get; private set; }
60 public string CachePath { get; private set; }
62 public TreeHash ServerHash { get; private set; }
64 public string TempPath { get; private set; }
68 get { return _blocks.Count>0; }
71 readonly ConcurrentDictionary<int, string> _blocks = new ConcurrentDictionary<int, string>();
72 readonly ConcurrentDictionary<string, string> _orphanBlocks = new ConcurrentDictionary<string, string>();
74 [ContractInvariantMethod]
75 private void Invariants()
77 Contract.Invariant(Path.IsPathRooted(CachePath));
78 Contract.Invariant(Path.IsPathRooted(FilePath));
79 Contract.Invariant(Path.IsPathRooted(TempPath));
80 Contract.Invariant(!Path.IsPathRooted(RelativePath));
81 Contract.Invariant(_blocks!=null);
82 Contract.Invariant(_orphanBlocks!=null);
83 Contract.Invariant(ServerHash!=null);
86 public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash)
88 if (String.IsNullOrWhiteSpace(cachePath))
89 throw new ArgumentNullException("cachePath");
90 if (!Path.IsPathRooted(cachePath))
91 throw new ArgumentException("The cachePath must be rooted", "cachePath");
93 if (string.IsNullOrWhiteSpace(filePath))
94 throw new ArgumentNullException("filePath");
95 if (!Path.IsPathRooted(filePath))
96 throw new ArgumentException("The filePath must be rooted", "filePath");
98 if (string.IsNullOrWhiteSpace(relativePath))
99 throw new ArgumentNullException("relativePath");
100 if (Path.IsPathRooted(relativePath))
101 throw new ArgumentException("The relativePath must NOT be rooted", "relativePath");
103 if (serverHash == null)
104 throw new ArgumentNullException("serverHash");
105 Contract.EndContractBlock();
109 RelativePath=relativePath;
110 ServerHash = serverHash;
111 //The file will be stored in a temporary location while downloading with an extension .download
112 TempPath = Path.Combine(CachePath, RelativePath + ".download");
114 //Need to calculate the directory path because RelativePath may include folders
115 var directoryPath = Path.GetDirectoryName(TempPath);
116 //directoryPath CAN be null if TempPath is a root path
117 if (String.IsNullOrWhiteSpace(directoryPath))
118 throw new ArgumentException("TempPath");
119 //CachePath was absolute so directoryPath is absolute too
120 Contract.Assume(Path.IsPathRooted(directoryPath));
122 if (!Directory.Exists(directoryPath))
123 Directory.CreateDirectory(directoryPath);
125 LoadOrphans(directoryPath);
128 private void LoadOrphans(string directoryPath)
130 if (string.IsNullOrWhiteSpace(directoryPath))
131 throw new ArgumentNullException("directoryPath");
132 if (!Path.IsPathRooted(directoryPath))
133 throw new ArgumentException("The directoryPath must be rooted", "directoryPath");
134 if (ServerHash==null)
135 throw new InvalidOperationException("ServerHash wasn't initialized");
136 Contract.EndContractBlock();
138 var fileNamename = Path.GetFileName(FilePath);
139 var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*");
140 foreach (var orphan in orphans)
142 using (HashAlgorithm hasher = HashAlgorithm.Create(ServerHash.BlockHash))
144 var buffer=File.ReadAllBytes(orphan);
145 //The server truncates nulls before calculating hashes, have to do the same
146 //Find the last non-null byte, starting from the end
147 var lastByteIndex = Array.FindLastIndex(buffer, buffer.Length-1, aByte => aByte != 0);
148 //lastByteIndex may be -1 if the file was empty. We don't want to use that block file
149 if (lastByteIndex >= 0)
151 var binHash = hasher.ComputeHash(buffer, 0, lastByteIndex);
152 var hash = binHash.ToHashString();
153 _orphanBlocks[hash] = orphan;
162 if (String.IsNullOrWhiteSpace(FilePath))
163 throw new InvalidOperationException("FilePath is empty");
164 if (String.IsNullOrWhiteSpace(TempPath))
165 throw new InvalidOperationException("TempPath is empty");
166 Contract.EndContractBlock();
168 //Copy the file to a temporary location. Changes will be made to the
169 //temporary file, then it will replace the original file
170 if (File.Exists(FilePath))
171 File.Copy(FilePath, TempPath, true);
173 //Set the size of the file to the size specified in the treehash
174 //This will also create an empty file if the file doesn't exist
177 SetFileSize(TempPath, ServerHash.Bytes);
179 //Update the temporary file with the data from the blocks
180 using (var stream = File.OpenWrite(TempPath))
182 foreach (var block in _blocks)
184 var blockPath = block.Value;
185 var blockIndex = block.Key;
186 using (var blockStream = File.OpenRead(blockPath))
188 var offset = blockIndex*ServerHash.BlockSize;
189 stream.Seek(offset, SeekOrigin.Begin);
190 blockStream.CopyTo(stream);
199 private void SwapFiles()
201 if (String.IsNullOrWhiteSpace(FilePath))
202 throw new InvalidOperationException("FilePath is empty");
203 if (String.IsNullOrWhiteSpace(TempPath))
204 throw new InvalidOperationException("TempPath is empty");
205 Contract.EndContractBlock();
207 if (File.Exists(FilePath))
208 File.Replace(TempPath, FilePath, null, true);
210 File.Move(TempPath, FilePath);
213 private void ClearBlocks()
215 //Get all the the block paths, orphan or not
216 var paths= _blocks.Select(pair => pair.Value)
217 .Union(_orphanBlocks.Select(pair => pair.Value));
218 foreach (var filePath in paths)
220 File.Delete(filePath);
223 File.Delete(TempPath);
225 _orphanBlocks.Clear();
228 //Change the file's size, possibly truncating or adding to it
229 private void SetFileSize(string filePath, long fileSize)
231 if (String.IsNullOrWhiteSpace(filePath))
232 throw new ArgumentNullException("filePath");
233 if (!Path.IsPathRooted(filePath))
234 throw new ArgumentException("The filePath must be rooted", "filePath");
236 throw new ArgumentOutOfRangeException("fileSize");
237 Contract.EndContractBlock();
239 using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
241 stream.SetLength(fileSize);
245 /* //Check whether we should copy the local file to a temp path
246 private bool ShouldCopy(string localPath, string tempPath)
248 //No need to copy if there is no file
249 if (!File.Exists(localPath))
252 //If there is no temp file, go ahead and copy
253 if (!File.Exists(tempPath))
256 //If there is a temp file and is newer than the actual file, don't copy
257 var localLastWrite = File.GetLastWriteTime(localPath);
258 var tempLastWrite = File.GetLastWriteTime(tempPath);
260 //This could mean there is an interrupted download in progress
261 return (tempLastWrite < localLastWrite);
265 public bool UseOrphan(int blockIndex, string blockHash)
267 string blockPath=null;
268 if (_orphanBlocks.TryGetValue(blockHash,out blockPath))
270 _blocks[blockIndex] = blockPath;
276 public Task StoreBlock(int blockIndex,byte[] buffer)
278 var blockPath = String.Format("{0}.{1:000000}", TempPath, blockIndex);
279 _blocks[blockIndex] = blockPath;
280 //Remove any orphan files
281 if (File.Exists(blockPath))
282 File.Delete(blockPath);
284 return FileAsync.WriteAllBytes(blockPath, buffer);