using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Pithos.Network; namespace Pithos.Core.Agents { class BlockUpdater { public string FilePath { get; private set; } public string RelativePath { get; private set; } public string CachePath { get; private set; } public TreeHash ServerHash { get; private set; } public string TempPath { get; private set; } public bool HasBlocks { get { return _blocks.Count>0; } } readonly ConcurrentDictionary _blocks = new ConcurrentDictionary(); readonly ConcurrentDictionary _orphanBlocks = new ConcurrentDictionary(); [ContractInvariantMethod] private void Invariants() { Contract.Invariant(Path.IsPathRooted(CachePath)); Contract.Invariant(Path.IsPathRooted(FilePath)); Contract.Invariant(Path.IsPathRooted(TempPath)); Contract.Invariant(!Path.IsPathRooted(RelativePath)); Contract.Invariant(_blocks!=null); Contract.Invariant(_orphanBlocks!=null); Contract.Invariant(ServerHash!=null); } public BlockUpdater(string cachePath, string filePath, string relativePath,TreeHash serverHash) { if (String.IsNullOrWhiteSpace(cachePath)) throw new ArgumentNullException("cachePath"); if (!Path.IsPathRooted(cachePath)) throw new ArgumentException("The cachePath must be rooted", "cachePath"); if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException("filePath"); if (!Path.IsPathRooted(filePath)) throw new ArgumentException("The filePath must be rooted", "filePath"); if (string.IsNullOrWhiteSpace(relativePath)) throw new ArgumentNullException("relativePath"); if (Path.IsPathRooted(relativePath)) throw new ArgumentException("The relativePath must NOT be rooted", "relativePath"); if (serverHash == null) throw new ArgumentNullException("serverHash"); Contract.EndContractBlock(); CachePath=cachePath; FilePath = filePath; RelativePath=relativePath; ServerHash = serverHash; //The file will be stored in a temporary location while downloading with an extension .download TempPath = Path.Combine(CachePath, RelativePath + ".download"); //Need to calculate the directory path because RelativePath may include folders var directoryPath = Path.GetDirectoryName(TempPath); //directoryPath CAN be null if TempPath is a root path if (String.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentException("TempPath"); //CachePath was absolute so directoryPath is absolute too Contract.Assume(Path.IsPathRooted(directoryPath)); if (!Directory.Exists(directoryPath)) Directory.CreateDirectory(directoryPath); LoadOrphans(directoryPath); } private void LoadOrphans(string directoryPath) { if (string.IsNullOrWhiteSpace(directoryPath)) throw new ArgumentNullException("directoryPath"); if (!Path.IsPathRooted(directoryPath)) throw new ArgumentException("The directoryPath must be rooted", "directoryPath"); if (ServerHash==null) throw new InvalidOperationException("ServerHash wasn't initialized"); Contract.EndContractBlock(); var fileNamename = Path.GetFileName(FilePath); var orphans = Directory.GetFiles(directoryPath, fileNamename + ".*"); foreach (var orphan in orphans) { using (HashAlgorithm hasher = HashAlgorithm.Create(ServerHash.BlockHash)) { var buffer=File.ReadAllBytes(orphan); //The server truncates nulls before calculating hashes, have to do the same //Find the last non-null byte, starting from the end var lastByteIndex = Array.FindLastIndex(buffer, buffer.Length-1, aByte => aByte != 0); //lastByteIndex may be -1 if the file was empty. We don't want to use that block file if (lastByteIndex >= 0) { var binHash = hasher.ComputeHash(buffer, 0, lastByteIndex); var hash = binHash.ToHashString(); _orphanBlocks[hash] = orphan; } } } } public void Commit() { if (String.IsNullOrWhiteSpace(FilePath)) throw new InvalidOperationException("FilePath is empty"); if (String.IsNullOrWhiteSpace(TempPath)) throw new InvalidOperationException("TempPath is empty"); Contract.EndContractBlock(); //Copy the file to a temporary location. Changes will be made to the //temporary file, then it will replace the original file if (File.Exists(FilePath)) File.Copy(FilePath, TempPath, true); //Set the size of the file to the size specified in the treehash //This will also create an empty file if the file doesn't exist SetFileSize(TempPath, ServerHash.Bytes); //Update the temporary file with the data from the blocks using (var stream = File.OpenWrite(TempPath)) { foreach (var block in _blocks) { var blockPath = block.Value; var blockIndex = block.Key; using (var blockStream = File.OpenRead(blockPath)) { var offset = blockIndex*ServerHash.BlockSize; stream.Seek(offset, SeekOrigin.Begin); blockStream.CopyTo(stream); } } } SwapFiles(); ClearBlocks(); } private void SwapFiles() { if (String.IsNullOrWhiteSpace(FilePath)) throw new InvalidOperationException("FilePath is empty"); if (String.IsNullOrWhiteSpace(TempPath)) throw new InvalidOperationException("TempPath is empty"); Contract.EndContractBlock(); if (File.Exists(FilePath)) File.Replace(TempPath, FilePath, null, true); else File.Move(TempPath, FilePath); } private void ClearBlocks() { //Get all the the block paths, orphan or not var paths= _blocks.Select(pair => pair.Value) .Union(_orphanBlocks.Select(pair => pair.Value)); foreach (var filePath in paths) { File.Delete(filePath); } File.Delete(TempPath); _blocks.Clear(); _orphanBlocks.Clear(); } //Change the file's size, possibly truncating or adding to it private void SetFileSize(string filePath, long fileSize) { if (String.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException("filePath"); if (!Path.IsPathRooted(filePath)) throw new ArgumentException("The filePath must be rooted", "filePath"); if (fileSize < 0) throw new ArgumentOutOfRangeException("fileSize"); Contract.EndContractBlock(); using (var stream = File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write)) { stream.SetLength(fileSize); } } /* //Check whether we should copy the local file to a temp path private bool ShouldCopy(string localPath, string tempPath) { //No need to copy if there is no file if (!File.Exists(localPath)) return false; //If there is no temp file, go ahead and copy if (!File.Exists(tempPath)) return true; //If there is a temp file and is newer than the actual file, don't copy var localLastWrite = File.GetLastWriteTime(localPath); var tempLastWrite = File.GetLastWriteTime(tempPath); //This could mean there is an interrupted download in progress return (tempLastWrite < localLastWrite); }*/ public bool UseOrphan(int blockIndex, string blockHash) { string blockPath=null; if (_orphanBlocks.TryGetValue(blockHash,out blockPath)) { _blocks[blockIndex] = blockPath; return true; } return false; } public Task StoreBlock(int blockIndex,byte[] buffer) { var blockPath = String.Format("{0}.{1:000000}", TempPath, blockIndex); _blocks[blockIndex] = blockPath; //Remove any orphan files if (File.Exists(blockPath)) File.Delete(blockPath); return FileAsync.WriteAllBytes(blockPath, buffer); } } }