root / trunk / Pithos.Core / Agents / BlockUpdater.cs @ 73cdd135
History | View | Annotate | Download (9.6 kB)
1 |
using System; |
---|---|
2 |
using System.Collections.Concurrent; |
3 |
using System.Collections.Generic; |
4 |
using System.Diagnostics.Contracts; |
5 |
using System.IO; |
6 |
using System.Linq; |
7 |
using System.Security.Cryptography; |
8 |
using System.Text; |
9 |
using System.Threading.Tasks; |
10 |
using Pithos.Network; |
11 |
|
12 |
namespace Pithos.Core.Agents |
13 |
{ |
14 |
class BlockUpdater |
15 |
{ |
16 |
public string FilePath { get; private set; } |
17 |
public string RelativePath { get; private set; } |
18 |
|
19 |
public string CachePath { get; private set; } |
20 |
|
21 |
public TreeHash ServerHash { get; private set; } |
22 |
|
23 |
public string TempPath { get; private set; } |
24 |
|
25 |
public bool HasBlocks |
26 |
{ |
27 |
get { return _blocks.Count>0; } |
28 |
} |
29 |
|
30 |
readonly ConcurrentDictionary<int, string> _blocks = new ConcurrentDictionary<int, string>(); |
31 |
readonly ConcurrentDictionary<string, string> _orphanBlocks = new ConcurrentDictionary<string, string>(); |
32 |
|
33 |
[ContractInvariantMethod] |
34 |
private void Invariants() |
35 |
{ |
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); |
43 |
} |
44 |
|
45 |
public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash) |
46 |
{ |
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"); |
51 |
|
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"); |
56 |
|
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"); |
61 |
|
62 |
if (serverHash == null) |
63 |
throw new ArgumentNullException("serverHash"); |
64 |
Contract.EndContractBlock(); |
65 |
|
66 |
CachePath=cachePath; |
67 |
FilePath = filePath; |
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"); |
72 |
|
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)); |
80 |
|
81 |
if (!Directory.Exists(directoryPath)) |
82 |
Directory.CreateDirectory(directoryPath); |
83 |
|
84 |
LoadOrphans(directoryPath); |
85 |
} |
86 |
|
87 |
private void LoadOrphans(string directoryPath) |
88 |
{ |
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"); |
93 |
if (ServerHash==null) |
94 |
throw new InvalidOperationException("ServerHash wasn't initialized"); |
95 |
Contract.EndContractBlock(); |
96 |
|
97 |
var fileNamename = Path.GetFileName(FilePath); |
98 |
var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*"); |
99 |
foreach (var orphan in orphans) |
100 |
{ |
101 |
using (HashAlgorithm hasher = HashAlgorithm.Create(ServerHash.BlockHash)) |
102 |
{ |
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) |
109 |
{ |
110 |
var binHash = hasher.ComputeHash(buffer, 0, lastByteIndex); |
111 |
var hash = binHash.ToHashString(); |
112 |
_orphanBlocks[hash] = orphan; |
113 |
} |
114 |
} |
115 |
} |
116 |
} |
117 |
|
118 |
|
119 |
public void Commit() |
120 |
{ |
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(); |
126 |
|
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); |
131 |
|
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 |
134 |
|
135 |
|
136 |
SetFileSize(TempPath, ServerHash.Bytes); |
137 |
|
138 |
//Update the temporary file with the data from the blocks |
139 |
using (var stream = File.OpenWrite(TempPath)) |
140 |
{ |
141 |
foreach (var block in _blocks) |
142 |
{ |
143 |
var blockPath = block.Value; |
144 |
var blockIndex = block.Key; |
145 |
using (var blockStream = File.OpenRead(blockPath)) |
146 |
{ |
147 |
var offset = blockIndex*ServerHash.BlockSize; |
148 |
stream.Seek(offset, SeekOrigin.Begin); |
149 |
blockStream.CopyTo(stream); |
150 |
} |
151 |
} |
152 |
} |
153 |
SwapFiles(); |
154 |
|
155 |
ClearBlocks(); |
156 |
} |
157 |
|
158 |
private void SwapFiles() |
159 |
{ |
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(); |
165 |
|
166 |
if (File.Exists(FilePath)) |
167 |
File.Replace(TempPath, FilePath, null, true); |
168 |
else |
169 |
File.Move(TempPath, FilePath); |
170 |
} |
171 |
|
172 |
private void ClearBlocks() |
173 |
{ |
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) |
178 |
{ |
179 |
File.Delete(filePath); |
180 |
} |
181 |
|
182 |
File.Delete(TempPath); |
183 |
_blocks.Clear(); |
184 |
_orphanBlocks.Clear(); |
185 |
} |
186 |
|
187 |
//Change the file's size, possibly truncating or adding to it |
188 |
private void SetFileSize(string filePath, long fileSize) |
189 |
{ |
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"); |
194 |
if (fileSize < 0) |
195 |
throw new ArgumentOutOfRangeException("fileSize"); |
196 |
Contract.EndContractBlock(); |
197 |
|
198 |
using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write)) |
199 |
{ |
200 |
stream.SetLength(fileSize); |
201 |
} |
202 |
} |
203 |
|
204 |
/* //Check whether we should copy the local file to a temp path |
205 |
private bool ShouldCopy(string localPath, string tempPath) |
206 |
{ |
207 |
//No need to copy if there is no file |
208 |
if (!File.Exists(localPath)) |
209 |
return false; |
210 |
|
211 |
//If there is no temp file, go ahead and copy |
212 |
if (!File.Exists(tempPath)) |
213 |
return true; |
214 |
|
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); |
218 |
|
219 |
//This could mean there is an interrupted download in progress |
220 |
return (tempLastWrite < localLastWrite); |
221 |
}*/ |
222 |
|
223 |
|
224 |
public bool UseOrphan(int blockIndex, string blockHash) |
225 |
{ |
226 |
string blockPath=null; |
227 |
if (_orphanBlocks.TryGetValue(blockHash,out blockPath)) |
228 |
{ |
229 |
_blocks[blockIndex] = blockPath; |
230 |
return true; |
231 |
} |
232 |
return false; |
233 |
} |
234 |
|
235 |
public Task StoreBlock(int blockIndex,byte[] buffer) |
236 |
{ |
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); |
242 |
|
243 |
return FileAsync.WriteAllBytes(blockPath, buffer); |
244 |
} |
245 |
|
246 |
|
247 |
|
248 |
} |
249 |
} |