2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Diagnostics.Contracts;
7 using System.Security.Cryptography;
9 using System.Threading.Tasks;
12 namespace Pithos.Core.Agents
16 public string FilePath { get; private set; }
17 public string RelativePath { get; private set; }
19 public string CachePath { get; private set; }
21 public TreeHash ServerHash { get; private set; }
23 public string TempPath { get; private set; }
27 get { return _blocks.Count>0; }
30 readonly ConcurrentDictionary<int, string> _blocks = new ConcurrentDictionary<int, string>();
31 readonly ConcurrentDictionary<string, string> _orphanBlocks = new ConcurrentDictionary<string, string>();
33 [ContractInvariantMethod]
34 private void Invariants()
36 Contract.Invariant(Path.IsPathRooted(CachePath));
37 Contract.Invariant(Path.IsPathRooted(FilePath));
38 Contract.Invariant(Path.IsPathRooted(TempPath));
39 Contract.Invariant(!Path.IsPathRooted(RelativePath));
40 Contract.Invariant(_blocks!=null);
41 Contract.Invariant(_orphanBlocks!=null);
42 Contract.Invariant(ServerHash!=null);
45 public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash)
47 if (String.IsNullOrWhiteSpace(cachePath))
48 throw new ArgumentNullException("cachePath");
49 if (!Path.IsPathRooted(cachePath))
50 throw new ArgumentException("The cachePath must be rooted", "cachePath");
52 if (string.IsNullOrWhiteSpace(filePath))
53 throw new ArgumentNullException("filePath");
54 if (!Path.IsPathRooted(filePath))
55 throw new ArgumentException("The filePath must be rooted", "filePath");
57 if (string.IsNullOrWhiteSpace(relativePath))
58 throw new ArgumentNullException("relativePath");
59 if (Path.IsPathRooted(relativePath))
60 throw new ArgumentException("The relativePath must NOT be rooted", "relativePath");
62 if (serverHash == null)
63 throw new ArgumentNullException("serverHash");
64 Contract.EndContractBlock();
68 RelativePath=relativePath;
69 ServerHash = serverHash;
70 //The file will be stored in a temporary location while downloading with an extension .download
71 TempPath = Path.Combine(CachePath, RelativePath + ".download");
73 //Need to calculate the directory path because RelativePath may include folders
74 var directoryPath = Path.GetDirectoryName(TempPath);
75 //directoryPath CAN be null if TempPath is a root path
76 if (String.IsNullOrWhiteSpace(directoryPath))
77 throw new ArgumentException("TempPath");
78 //CachePath was absolute so directoryPath is absolute too
79 Contract.Assume(Path.IsPathRooted(directoryPath));
81 if (!Directory.Exists(directoryPath))
82 Directory.CreateDirectory(directoryPath);
84 LoadOrphans(directoryPath);
87 private void LoadOrphans(string directoryPath)
89 if (string.IsNullOrWhiteSpace(directoryPath))
90 throw new ArgumentNullException("directoryPath");
91 if (!Path.IsPathRooted(directoryPath))
92 throw new ArgumentException("The directoryPath must be rooted", "directoryPath");
94 throw new InvalidOperationException("ServerHash wasn't initialized");
95 Contract.EndContractBlock();
97 var fileNamename = Path.GetFileName(FilePath);
98 var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*");
99 foreach (var orphan in orphans)
101 using (HashAlgorithm hasher = HashAlgorithm.Create(ServerHash.BlockHash))
103 var buffer=File.ReadAllBytes(orphan);
104 //The server truncates nulls before calculating hashes, have to do the same
105 //Find the last non-null byte, starting from the end
106 var lastByteIndex = Array.FindLastIndex(buffer, buffer.Length-1, aByte => aByte != 0);
107 //lastByteIndex may be -1 if the file was empty. We don't want to use that block file
108 if (lastByteIndex >= 0)
110 var binHash = hasher.ComputeHash(buffer, 0, lastByteIndex);
111 var hash = binHash.ToHashString();
112 _orphanBlocks[hash] = orphan;
121 if (String.IsNullOrWhiteSpace(FilePath))
122 throw new InvalidOperationException("FilePath is empty");
123 if (String.IsNullOrWhiteSpace(TempPath))
124 throw new InvalidOperationException("TempPath is empty");
125 Contract.EndContractBlock();
127 //Copy the file to a temporary location. Changes will be made to the
128 //temporary file, then it will replace the original file
129 if (File.Exists(FilePath))
130 File.Copy(FilePath, TempPath, true);
132 //Set the size of the file to the size specified in the treehash
133 //This will also create an empty file if the file doesn't exist
136 SetFileSize(TempPath, ServerHash.Bytes);
138 //Update the temporary file with the data from the blocks
139 using (var stream = File.OpenWrite(TempPath))
141 foreach (var block in _blocks)
143 var blockPath = block.Value;
144 var blockIndex = block.Key;
145 using (var blockStream = File.OpenRead(blockPath))
147 var offset = blockIndex*ServerHash.BlockSize;
148 stream.Seek(offset, SeekOrigin.Begin);
149 blockStream.CopyTo(stream);
158 private void SwapFiles()
160 if (String.IsNullOrWhiteSpace(FilePath))
161 throw new InvalidOperationException("FilePath is empty");
162 if (String.IsNullOrWhiteSpace(TempPath))
163 throw new InvalidOperationException("TempPath is empty");
164 Contract.EndContractBlock();
166 if (File.Exists(FilePath))
167 File.Replace(TempPath, FilePath, null, true);
169 File.Move(TempPath, FilePath);
172 private void ClearBlocks()
174 //Get all the the block paths, orphan or not
175 var paths= _blocks.Select(pair => pair.Value)
176 .Union(_orphanBlocks.Select(pair => pair.Value));
177 foreach (var filePath in paths)
179 File.Delete(filePath);
182 File.Delete(TempPath);
184 _orphanBlocks.Clear();
187 //Change the file's size, possibly truncating or adding to it
188 private void SetFileSize(string filePath, long fileSize)
190 if (String.IsNullOrWhiteSpace(filePath))
191 throw new ArgumentNullException("filePath");
192 if (!Path.IsPathRooted(filePath))
193 throw new ArgumentException("The filePath must be rooted", "filePath");
195 throw new ArgumentOutOfRangeException("fileSize");
196 Contract.EndContractBlock();
198 using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
200 stream.SetLength(fileSize);
204 /* //Check whether we should copy the local file to a temp path
205 private bool ShouldCopy(string localPath, string tempPath)
207 //No need to copy if there is no file
208 if (!File.Exists(localPath))
211 //If there is no temp file, go ahead and copy
212 if (!File.Exists(tempPath))
215 //If there is a temp file and is newer than the actual file, don't copy
216 var localLastWrite = File.GetLastWriteTime(localPath);
217 var tempLastWrite = File.GetLastWriteTime(tempPath);
219 //This could mean there is an interrupted download in progress
220 return (tempLastWrite < localLastWrite);
224 public bool UseOrphan(int blockIndex, string blockHash)
226 string blockPath=null;
227 if (_orphanBlocks.TryGetValue(blockHash,out blockPath))
229 _blocks[blockIndex] = blockPath;
235 public Task StoreBlock(int blockIndex,byte[] buffer)
237 var blockPath = String.Format("{0}.{1:000000}", TempPath, blockIndex);
238 _blocks[blockIndex] = blockPath;
239 //Remove any orphan files
240 if (File.Exists(blockPath))
241 File.Delete(blockPath);
243 return FileAsync.WriteAllBytes(blockPath, buffer);