3eff1c6811e0c2f92fa34940767b8244bb59d324
[pithos-ms-client] / trunk%2FPithos.Core%2FAgents%2FBlockUpdater.cs
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.Reflection;
49 using System.Security.Cryptography;
50 using System.Text;
51 using System.Threading.Tasks;
52 using Pithos.Network;
53
54 namespace Pithos.Core.Agents
55 {
56     class BlockUpdater
57     {
58         private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
59
60         public string FilePath { get; private set; }
61         public string RelativePath { get; private set; }
62
63         public string CachePath { get; private set; }
64
65         public TreeHash ServerHash { get; private set; }
66
67         public string TempPath { get; private set; }
68
69         public bool HasBlocks
70         {
71             get { return _blocks.Count>0; }            
72         }
73
74         readonly ConcurrentDictionary<int, string> _blocks = new ConcurrentDictionary<int, string>();
75         readonly ConcurrentDictionary<string, string> _orphanBlocks = new ConcurrentDictionary<string, string>();
76
77         [ContractInvariantMethod]
78         private void Invariants()
79         {
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);
87         }
88
89         public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash)
90         {   
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");
95             
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");
100             
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");
105
106             if (serverHash == null)
107                 throw new ArgumentNullException("serverHash");
108             Contract.EndContractBlock();
109
110             CachePath=cachePath;
111             FilePath = filePath;
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");
116             
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));
124             
125             if (!Directory.Exists(directoryPath))
126                 Directory.CreateDirectory(directoryPath);
127
128             LoadOrphans(directoryPath);
129         }
130
131         private void LoadOrphans(string directoryPath)
132         {
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();
140
141             var fileNamename = Path.GetFileName(FilePath);
142             var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*");
143             foreach (var orphan in orphans)
144             {
145                 using (HashAlgorithm hasher = HashAlgorithm.Create(ServerHash.BlockHash))
146                 {
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)
153                     {
154                         var binHash = hasher.ComputeHash(buffer, 0, lastByteIndex);
155                         var hash = binHash.ToHashString();
156                         _orphanBlocks[hash] = orphan;
157                     }
158                 }
159             }
160         }
161
162
163         public void Commit()
164         {
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();
170
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);
175
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                        
178             
179             
180             SetFileSize(TempPath, ServerHash.Bytes);
181
182             //Update the temporary file with the data from the blocks
183             using (var stream = File.OpenWrite(TempPath))
184             {
185                 foreach (var block in _blocks)
186                 {
187                     var blockPath = block.Value;
188                     var blockIndex = block.Key;
189                     using (var blockStream = File.OpenRead(blockPath))
190                     {                        
191                         long offset = blockIndex*ServerHash.BlockSize;
192                         stream.Seek(offset, SeekOrigin.Begin);
193                         blockStream.CopyTo(stream);
194                     }
195                 }
196             }
197             SwapFiles();
198
199             ClearBlocks();
200         }
201
202         private void SwapFiles()
203         {
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();
209
210             if (File.Exists(FilePath))
211                 File.Replace(TempPath, FilePath, null, true);
212             else
213             {
214                 var targetDirectory = Path.GetDirectoryName(FilePath);
215                 if (!Directory.Exists(targetDirectory))
216                     Directory.CreateDirectory(targetDirectory);
217                 File.Move(TempPath, FilePath);
218             }
219         }
220
221         private void ClearBlocks()
222         {
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)
229             {
230                 File.Delete(filePath);
231             }
232
233             File.Delete(TempPath);
234             _blocks.Clear();
235             _orphanBlocks.Clear();
236         }
237
238         //Change the file's size, possibly truncating or adding to it
239         private  void SetFileSize(string filePath, long fileSize)
240         {
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");
245             if (fileSize < 0)
246                 throw new ArgumentOutOfRangeException("fileSize");
247             Contract.EndContractBlock();
248
249             using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
250             {
251                 stream.SetLength(fileSize);
252             }
253         }
254
255        /* //Check whether we should copy the local file to a temp path        
256         private  bool ShouldCopy(string localPath, string tempPath)
257         {
258             //No need to copy if there is no file
259             if (!File.Exists(localPath))
260                 return false;
261
262             //If there is no temp file, go ahead and copy
263             if (!File.Exists(tempPath))
264                 return true;
265
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);
269
270             //This could mean there is an interrupted download in progress
271             return (tempLastWrite < localLastWrite);
272         }*/
273
274
275         public bool UseOrphan(int blockIndex, string blockHash)
276         {
277             string blockPath=null;
278             if (_orphanBlocks.TryGetValue(blockHash,out blockPath))
279             {
280                 _blocks[blockIndex] = blockPath;
281                 return true;
282             }
283             return false;
284         }
285
286         public Task StoreBlock(int blockIndex,byte[] buffer)
287         {
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);
293
294             return FileAsync.WriteAllBytes(blockPath, buffer);
295         }
296
297        
298
299     }
300 }