root / trunk / Pithos.Core / Agents / BlockUpdater.cs @ 174bbb6e
History | View | Annotate | Download (11.6 kB)
1 |
#region |
---|---|
2 |
/* ----------------------------------------------------------------------- |
3 |
* <copyright file="BlockUpdater.cs" company="GRNet"> |
4 |
* |
5 |
* Copyright 2011-2012 GRNET S.A. All rights reserved. |
6 |
* |
7 |
* Redistribution and use in source and binary forms, with or |
8 |
* without modification, are permitted provided that the following |
9 |
* conditions are met: |
10 |
* |
11 |
* 1. Redistributions of source code must retain the above |
12 |
* copyright notice, this list of conditions and the following |
13 |
* disclaimer. |
14 |
* |
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. |
19 |
* |
20 |
* |
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. |
33 |
* |
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. |
38 |
* </copyright> |
39 |
* ----------------------------------------------------------------------- |
40 |
*/ |
41 |
#endregion |
42 |
using System; |
43 |
using System.Collections.Concurrent; |
44 |
using System.Collections.Generic; |
45 |
using System.Diagnostics.Contracts; |
46 |
using System.IO; |
47 |
using System.Linq; |
48 |
using System.Security.Cryptography; |
49 |
using System.Text; |
50 |
using System.Threading.Tasks; |
51 |
using Pithos.Network; |
52 |
|
53 |
namespace Pithos.Core.Agents |
54 |
{ |
55 |
class BlockUpdater |
56 |
{ |
57 |
public string FilePath { get; private set; } |
58 |
public string RelativePath { get; private set; } |
59 |
|
60 |
public string CachePath { get; private set; } |
61 |
|
62 |
public TreeHash ServerHash { get; private set; } |
63 |
|
64 |
public string TempPath { get; private set; } |
65 |
|
66 |
public bool HasBlocks |
67 |
{ |
68 |
get { return _blocks.Count>0; } |
69 |
} |
70 |
|
71 |
readonly ConcurrentDictionary<int, string> _blocks = new ConcurrentDictionary<int, string>(); |
72 |
readonly ConcurrentDictionary<string, string> _orphanBlocks = new ConcurrentDictionary<string, string>(); |
73 |
|
74 |
[ContractInvariantMethod] |
75 |
private void Invariants() |
76 |
{ |
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); |
84 |
} |
85 |
|
86 |
public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash) |
87 |
{ |
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"); |
92 |
|
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"); |
97 |
|
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"); |
102 |
|
103 |
if (serverHash == null) |
104 |
throw new ArgumentNullException("serverHash"); |
105 |
Contract.EndContractBlock(); |
106 |
|
107 |
CachePath=cachePath; |
108 |
FilePath = filePath; |
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"); |
113 |
|
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)); |
121 |
|
122 |
if (!Directory.Exists(directoryPath)) |
123 |
Directory.CreateDirectory(directoryPath); |
124 |
|
125 |
LoadOrphans(directoryPath); |
126 |
} |
127 |
|
128 |
private void LoadOrphans(string directoryPath) |
129 |
{ |
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(); |
137 |
|
138 |
var fileNamename = Path.GetFileName(FilePath); |
139 |
var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*"); |
140 |
foreach (var orphan in orphans) |
141 |
{ |
142 |
using (HashAlgorithm hasher = HashAlgorithm.Create(ServerHash.BlockHash)) |
143 |
{ |
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) |
150 |
{ |
151 |
var binHash = hasher.ComputeHash(buffer, 0, lastByteIndex); |
152 |
var hash = binHash.ToHashString(); |
153 |
_orphanBlocks[hash] = orphan; |
154 |
} |
155 |
} |
156 |
} |
157 |
} |
158 |
|
159 |
|
160 |
public void Commit() |
161 |
{ |
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(); |
167 |
|
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); |
172 |
|
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 |
175 |
|
176 |
|
177 |
SetFileSize(TempPath, ServerHash.Bytes); |
178 |
|
179 |
//Update the temporary file with the data from the blocks |
180 |
using (var stream = File.OpenWrite(TempPath)) |
181 |
{ |
182 |
foreach (var block in _blocks) |
183 |
{ |
184 |
var blockPath = block.Value; |
185 |
var blockIndex = block.Key; |
186 |
using (var blockStream = File.OpenRead(blockPath)) |
187 |
{ |
188 |
var offset = blockIndex*ServerHash.BlockSize; |
189 |
stream.Seek(offset, SeekOrigin.Begin); |
190 |
blockStream.CopyTo(stream); |
191 |
} |
192 |
} |
193 |
} |
194 |
SwapFiles(); |
195 |
|
196 |
ClearBlocks(); |
197 |
} |
198 |
|
199 |
private void SwapFiles() |
200 |
{ |
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(); |
206 |
|
207 |
if (File.Exists(FilePath)) |
208 |
File.Replace(TempPath, FilePath, null, true); |
209 |
else |
210 |
{ |
211 |
var targetDirectory = Path.GetDirectoryName(FilePath); |
212 |
if (!Directory.Exists(targetDirectory)) |
213 |
Directory.CreateDirectory(targetDirectory); |
214 |
File.Move(TempPath, FilePath); |
215 |
} |
216 |
} |
217 |
|
218 |
private void ClearBlocks() |
219 |
{ |
220 |
//Get all the the block paths, orphan or not |
221 |
var paths= _blocks.Select(pair => pair.Value) |
222 |
.Union(_orphanBlocks.Select(pair => pair.Value)); |
223 |
foreach (var filePath in paths) |
224 |
{ |
225 |
File.Delete(filePath); |
226 |
} |
227 |
|
228 |
File.Delete(TempPath); |
229 |
_blocks.Clear(); |
230 |
_orphanBlocks.Clear(); |
231 |
} |
232 |
|
233 |
//Change the file's size, possibly truncating or adding to it |
234 |
private void SetFileSize(string filePath, long fileSize) |
235 |
{ |
236 |
if (String.IsNullOrWhiteSpace(filePath)) |
237 |
throw new ArgumentNullException("filePath"); |
238 |
if (!Path.IsPathRooted(filePath)) |
239 |
throw new ArgumentException("The filePath must be rooted", "filePath"); |
240 |
if (fileSize < 0) |
241 |
throw new ArgumentOutOfRangeException("fileSize"); |
242 |
Contract.EndContractBlock(); |
243 |
|
244 |
using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write)) |
245 |
{ |
246 |
stream.SetLength(fileSize); |
247 |
} |
248 |
} |
249 |
|
250 |
/* //Check whether we should copy the local file to a temp path |
251 |
private bool ShouldCopy(string localPath, string tempPath) |
252 |
{ |
253 |
//No need to copy if there is no file |
254 |
if (!File.Exists(localPath)) |
255 |
return false; |
256 |
|
257 |
//If there is no temp file, go ahead and copy |
258 |
if (!File.Exists(tempPath)) |
259 |
return true; |
260 |
|
261 |
//If there is a temp file and is newer than the actual file, don't copy |
262 |
var localLastWrite = File.GetLastWriteTime(localPath); |
263 |
var tempLastWrite = File.GetLastWriteTime(tempPath); |
264 |
|
265 |
//This could mean there is an interrupted download in progress |
266 |
return (tempLastWrite < localLastWrite); |
267 |
}*/ |
268 |
|
269 |
|
270 |
public bool UseOrphan(int blockIndex, string blockHash) |
271 |
{ |
272 |
string blockPath=null; |
273 |
if (_orphanBlocks.TryGetValue(blockHash,out blockPath)) |
274 |
{ |
275 |
_blocks[blockIndex] = blockPath; |
276 |
return true; |
277 |
} |
278 |
return false; |
279 |
} |
280 |
|
281 |
public Task StoreBlock(int blockIndex,byte[] buffer) |
282 |
{ |
283 |
var blockPath = String.Format("{0}.{1:000000}", TempPath, blockIndex); |
284 |
_blocks[blockIndex] = blockPath; |
285 |
//Remove any orphan files |
286 |
if (File.Exists(blockPath)) |
287 |
File.Delete(blockPath); |
288 |
|
289 |
return FileAsync.WriteAllBytes(blockPath, buffer); |
290 |
} |
291 |
|
292 |
|
293 |
|
294 |
} |
295 |
} |