f882db9fc65c30b59a60e600e9e016c4d6bbe5ea
[pithos-ms-client] / trunk%2FPithos.Core%2FAgents%2FBlockUpdater.cs
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 }