Synch seems OK. Identified problem with poll differencer
[pithos-ms-client] / trunk / Pithos.Core / Agents / BlockUpdater.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.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                 File.Move(TempPath, FilePath);
211         }
212
213         private void ClearBlocks()
214         {
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)
219             {
220                 File.Delete(filePath);
221             }
222
223             File.Delete(TempPath);
224             _blocks.Clear();
225             _orphanBlocks.Clear();
226         }
227
228         //Change the file's size, possibly truncating or adding to it
229         private  void SetFileSize(string filePath, long fileSize)
230         {
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");
235             if (fileSize < 0)
236                 throw new ArgumentOutOfRangeException("fileSize");
237             Contract.EndContractBlock();
238
239             using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write))
240             {
241                 stream.SetLength(fileSize);
242             }
243         }
244
245        /* //Check whether we should copy the local file to a temp path        
246         private  bool ShouldCopy(string localPath, string tempPath)
247         {
248             //No need to copy if there is no file
249             if (!File.Exists(localPath))
250                 return false;
251
252             //If there is no temp file, go ahead and copy
253             if (!File.Exists(tempPath))
254                 return true;
255
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);
259
260             //This could mean there is an interrupted download in progress
261             return (tempLastWrite < localLastWrite);
262         }*/
263
264
265         public bool UseOrphan(int blockIndex, string blockHash)
266         {
267             string blockPath=null;
268             if (_orphanBlocks.TryGetValue(blockHash,out blockPath))
269             {
270                 _blocks[blockIndex] = blockPath;
271                 return true;
272             }
273             return false;
274         }
275
276         public Task StoreBlock(int blockIndex,byte[] buffer)
277         {
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);
283
284             return FileAsync.WriteAllBytes(blockPath, buffer);
285         }
286
287        
288
289     }
290 }