private Changelist ApplyClientChanges( Client client, ChangeNode clientNode, IDictionary<string, ClientChange> clientChangesByFullName, IDictionary<string, Blob> presentHashes ) { Dictionary<string, File> fileCache = new Dictionary<string, File>(); Queue<ChangeNode> queue = new Queue<ChangeNode>(); fileCache.Add(clientNode.FullName, GetRootFolder()); foreach (var node in clientNode.Nodes.Values) queue.Enqueue(node); Changelist changelist = new Changelist { ClientId = client.Id, TimeStamp = DateTime.UtcNow }; var quotaCharge = new Dictionary<User, long>(); while (queue.Count != 0) { var node = queue.Dequeue(); // There is no need to process files that haven't changed. if (node.Type == ChangeType.None && !node.IsFolder) continue; // Find the associated database file object. var parentFolder = (Folder)fileCache[node.Parent.FullName]; var file = parentFolder.Files.AsQueryable() .Where(f => f.Name == node.Name) .FirstOrDefault(); bool createChange = false; switch (node.Type) { case ChangeType.Add: bool createFolder = false; bool createDocument = false; bool createDocumentVersion = false; bool setInvitation = false; bool setDisplayName = false; bool undeleted = false; bool replaced = false; if (file == null) { if (node.IsFolder) { // Nothing -> Folder // Create the folder. createFolder = true; setInvitation = true; } else { // Nothing -> Document // Create the document and the first version. createDocument = true; } } else if (file is Folder) { if (node.IsFolder) { // Folder -> Folder // Only a possible rename or invitation change is needed. setInvitation = true; setDisplayName = true; } else { // Folder -> Document // The folder is implicitly being deleted. RenameAndDeleteConflictingFile(parentFolder, file, "Deleted", quotaCharge, changelist); replaced = true; createDocument = true; } } else if (file is Document) { if (node.IsFolder) { // Document -> Folder // The document is implicitly being deleted. RenameAndDeleteConflictingFile(parentFolder, file, "Deleted", quotaCharge, changelist); replaced = true; createFolder = true; setInvitation = true; } else { // Document -> Document // Add a version. createDocumentVersion = true; setDisplayName = true; } } // Apply the required changes. if (file != null && !replaced) { // Undelete the file if it is deleted. if (file.State != ObjectState.Normal) { file.State = ObjectState.Normal; undeleted = true; createChange = true; } } if (createFolder) { file = _context.Folders.Add(new Folder { Name = node.Name, DisplayName = clientChangesByFullName[node.FullName].DisplayName ?? node.Name, ParentFolder = parentFolder, Owner = parentFolder.Owner }); createChange = true; } else if (createDocument) { file = _context.Documents.Add(new Document { Name = node.Name, DisplayName = clientChangesByFullName[node.FullName].DisplayName ?? node.Name, ParentFolder = parentFolder }); createDocumentVersion = true; createChange = true; } if (createDocumentVersion) { string hash = clientChangesByFullName[node.FullName].Hash.ToUpperInvariant(); bool identicalVersion = false; long latestSize = 0; if (!createDocument) { var latestBlob = GetLastDocumentVersion((Document)file).Blob; latestSize = latestBlob.Size; if (hash == latestBlob.Hash) identicalVersion = true; } if (!identicalVersion) { var blob = presentHashes[hash]; _context.DocumentVersions.Add(new DocumentVersion { TimeStamp = DateTime.UtcNow, ClientId = client.Id, Document = (Document)file, Blob = blob }); createChange = true; AddQuotaCharge(quotaCharge, file.ParentFolder.Owner, (undeleted ? 0 : -latestSize) + blob.Size); } else if (undeleted) { AddQuotaCharge(quotaCharge, file.ParentFolder.Owner, latestSize); } } if (setInvitation) { long? invitationId = clientChangesByFullName[node.FullName].InvitationId; if (invitationId != null) { if (invitationId.Value != 0 && ((Folder)file).OwnerId == client.UserId) { var invitation = ( from i in client.User.Invitations.AsQueryable() where i.Id == invitationId.Value select i ).SingleOrDefault(); if (invitation != null) { // Remove all other folders that link to this invitation. invitation.AcceptedFolders.Clear(); invitation.AcceptedFolders.Add((Folder)file); } } else { ((Folder)file).InvitationId = null; } } } if (setDisplayName && !string.IsNullOrEmpty(clientChangesByFullName[node.FullName].DisplayName)) { if (file.DisplayName != clientChangesByFullName[node.FullName].DisplayName) { file.DisplayName = clientChangesByFullName[node.FullName].DisplayName; createChange = true; } } break; case ChangeType.SetDisplayName: if (file != null && !string.IsNullOrEmpty(clientChangesByFullName[node.FullName].DisplayName)) { if (file.DisplayName != clientChangesByFullName[node.FullName].DisplayName) { file.DisplayName = clientChangesByFullName[node.FullName].DisplayName; createChange = true; } } break; case ChangeType.Delete: if (file != null && file.State != ObjectState.Deleted) { SetFileState(file, ObjectState.Deleted, quotaCharge, changelist); createChange = true; } break; case ChangeType.Undelete: if (file != null && file is Document && file.State != ObjectState.Normal) { SetFileState(file, ObjectState.Normal, quotaCharge, changelist); createChange = true; } break; } if (createChange) { // Create the associated change object. changelist.Changes.Add(new Change { Type = node.Type, FullName = node.FullName, IsFolder = node.IsFolder }); } if (file != null) { fileCache.Add(node.FullName, file); if (node.Nodes != null) { foreach (var subNode in node.Nodes.Values) queue.Enqueue(subNode); } } } // Apply quotas. foreach (var pair in quotaCharge) { if (pair.Key.QuotaCharged + pair.Value > pair.Key.QuotaLimit) throw new Exception("Quota exceeded for user '" + pair.Key.Name + "'"); pair.Key.QuotaCharged += pair.Value; } if (changelist.Changes.Count != 0) _context.Changelists.Add(changelist); return changelist; }
private void Apply(File rootFolder, ChangeNode rootNode, Dictionary<string, ClientChange> clientChanges) { if (rootNode.Nodes == null) return; foreach (ChangeNode node in rootNode.Nodes.Values.ToList()) { File file = null; if (rootFolder.Files != null) rootFolder.Files.TryGetValue(node.Name, out file); switch (node.Type) { case ChangeType.None: { // Process files in the folder. if (node.Nodes != null && node.Nodes.Count != 0) Apply(file, node, clientChanges); // State management if (node.Nodes == null || node.Nodes.Count == 0) { clientChanges.Remove(node.FullName); // If it exists node.Parent.Nodes.Remove(node.Name); } if (_cancellationToken.IsCancellationRequested) return; } break; case ChangeType.Add: case ChangeType.Undelete: { string newDisplayName = clientChanges[node.FullName].DisplayName; string newFullDisplayName = GetLocalFullName(node.Parent.FullName) + "\\" + newDisplayName; if (rootFolder.Files == null) rootFolder.Files = new Dictionary<string, File>(); if (node.IsFolder) { // Add folder if (file != null && !file.IsFolder) { SalvageAndDeleteDocument(file, newFullDisplayName); file = null; } if (!System.IO.Directory.Exists(newFullDisplayName)) System.IO.Directory.CreateDirectory(newFullDisplayName); else if (file != null && file.DisplayName != newDisplayName) MoveFileOrDirectory(newFullDisplayName, newFullDisplayName); long invitationId = clientChanges[node.FullName].InvitationId.Value; WriteInvitationId(newFullDisplayName, invitationId); if (invitationId != 0 && (file == null || file.InvitationId != invitationId)) _state.NewInvitations[invitationId.ToString()] = node.FullName; if (file == null) { file = new File { FullName = node.FullName, Name = node.Name, DisplayName = newDisplayName, IsFolder = true }; rootFolder.Files[file.Name] = file; } else { file.DisplayName = newDisplayName; } // Process files in the folder. if (node.Nodes != null && node.Nodes.Count != 0) Apply(file, node, clientChanges); // State management if (node.Nodes == null || node.Nodes.Count == 0) { clientChanges.Remove(node.FullName); node.Parent.Nodes.Remove(node.Name); } if (_cancellationToken.IsCancellationRequested) return; } else { // Add document if (file != null && file.IsFolder) { SalvageAndDeleteFolder(file, newFullDisplayName); rootFolder.Files.Remove(file.Name); file = null; } if (System.IO.File.Exists(newFullDisplayName) && file != null && file.DisplayName != newDisplayName) { MoveFileOrDirectory(newFullDisplayName, newFullDisplayName); } string hash = clientChanges[node.FullName].Hash; bool valid = true; if (file == null || file.Hash != hash || !System.IO.File.Exists(newFullDisplayName)) { valid = DownloadDocument(hash, newFullDisplayName); } if (valid) { FileInfo info = new FileInfo(newFullDisplayName); file = new File { FullName = node.FullName, Name = node.Name, DisplayName = newDisplayName, IsFolder = false, Size = info.Length, Hash = hash, LastWriteTimeUtc = info.LastWriteTimeUtc }; rootFolder.Files[file.Name] = file; } // State management clientChanges.Remove(node.FullName); node.Parent.Nodes.Remove(node.Name); if (_cancellationToken.IsCancellationRequested) return; } } break; case ChangeType.SetDisplayName: { string newDisplayName = clientChanges[node.FullName].DisplayName; string newFullDisplayName = GetLocalFullName(node.Parent.FullName) + "\\" + newDisplayName; if (file != null && file.DisplayName != newDisplayName) { MoveFileOrDirectory(newFullDisplayName, newFullDisplayName); file.DisplayName = newDisplayName; } // State management clientChanges.Remove(node.FullName); node.Parent.Nodes.Remove(node.Name); if (_cancellationToken.IsCancellationRequested) return; } break; case ChangeType.Delete: { if (file != null) { if (file.IsFolder) SalvageAndDeleteFolder(file, GetLocalFullName(file.FullName)); else SalvageAndDeleteDocument(file, GetLocalFullName(file.FullName)); if (file.InvitationId != 0) { _state.Invitations.Remove(file.InvitationId.ToString()); // Remove the invitation root from our file system. _state.Root.Files.Remove("@" + file.InvitationId.ToString()); } rootFolder.Files.Remove(file.Name); } // State management clientChanges.Remove(node.FullName); node.Parent.Nodes.Remove(node.Name); if (_cancellationToken.IsCancellationRequested) return; } break; } } }
public ChangeNode ShallowClone(ChangeNode newParent) { return new ChangeNode { Name = Name, FullName = FullName, Type = Type, IsFolder = IsFolder, Parent = newParent }; }
private List<ClientChange> GetChangesForNode(ChangeNode rootNode, Client client) { var changes = new List<ClientChange>(); Dictionary<string, File> fileCache = new Dictionary<string, File>(); Queue<ChangeNode> queue = new Queue<ChangeNode>(); fileCache.Add(rootNode.FullName, GetRootFolder()); foreach (var node in rootNode.Nodes.Values) queue.Enqueue(node); while (queue.Count != 0) { var node = queue.Dequeue(); // There is no need to process files that haven't changed. if (node.Type == ChangeType.None && !node.IsFolder) continue; var parentFolder = (Folder)fileCache[node.Parent.FullName]; var file = parentFolder.Files.AsQueryable() .Where(f => f.Name == node.Name) .FirstOrDefault(); long size = 0; string hash = ""; string displayName = null; long invitationId = 0; if (file != null) { if (file is Document) { var blob = GetLastDocumentVersion((Document)file).Blob; size = blob.Size; hash = blob.Hash; } else if (file is Folder) { invitationId = ((Folder)file).InvitationId ?? 0; } displayName = file.DisplayName; fileCache.Add(node.FullName, file); if (node.Nodes != null) { foreach (var subNode in node.Nodes.Values) queue.Enqueue(subNode); } } changes.Add(new ClientChange { FullName = node.FullName, Type = node.Type, IsFolder = node.IsFolder, Size = size, Hash = hash, DisplayName = displayName ?? node.Name, InvitationId = invitationId }); } return TranslateClientChangesOut(changes, client); }
/// <summary> /// Merges <paramref name="other"/> with this node, assuming that /// <paramref name="other"/> occurred after the changes specified by this node. /// </summary> /// <param name="other">The more recent node.</param> public void SequentialMerge(ChangeNode other) { if (other.Nodes == null) return; foreach (var otherNode in other.Nodes.Values) { ChangeNode ourNode = null; if (Nodes != null && Nodes.TryGetValue(otherNode.Name, out ourNode)) { switch (otherNode.Type) { case ChangeType.Add: ourNode.Type = ChangeType.Add; if (ourNode.IsFolder != otherNode.IsFolder) { // The file was replaced by a folder, or vice versa. Delete all children. ourNode.IsFolder = otherNode.IsFolder; ourNode.Nodes = null; } break; case ChangeType.SetDisplayName: if (ourNode.Type == ChangeType.None) ourNode.Type = ChangeType.SetDisplayName; break; case ChangeType.Delete: ourNode.Type = ChangeType.Delete; // Recursive delete. ourNode.Nodes = null; break; case ChangeType.Undelete: if (ourNode.Type == ChangeType.None || ourNode.Type == ChangeType.SetDisplayName || ourNode.Type == ChangeType.Delete) { ourNode.Type = ChangeType.Undelete; } break; } } else { if (Nodes == null) Nodes = new Dictionary<string, ChangeNode>(); ourNode = otherNode.ShallowClone(this); Nodes.Add(ourNode.Name, ourNode); } if (ourNode.Type != ChangeType.Delete && ourNode.IsFolder) ourNode.SequentialMerge(otherNode); } }
/// <summary> /// Merges <paramref name="other"/> with this node, attempting to preserve /// data where possible. No assumptions are made about which changes were /// made first. It is assumed that <see cref="PreservingConflicts"/> returns /// true. /// /// Note: (a.PreservingMerge(b), a) is always equal to (b.PreservingMerge(a), b). /// </summary> /// <param name="other">The other node.</param> public void PreservingMerge(ChangeNode other) { if (other.Nodes == null) return; foreach (var otherNode in other.Nodes.Values) { ChangeNode ourNode = null; if (Nodes != null && Nodes.TryGetValue(otherNode.Name, out ourNode)) { switch (otherNode.Type) { case ChangeType.Add: if (ourNode.Type == ChangeType.Add && (!ourNode.IsFolder || !otherNode.IsFolder)) throw new Exception("Add conflicts with Add."); if (ourNode.Type == ChangeType.Undelete && otherNode.IsFolder) throw new Exception("Add folder conflicts with Undelete file."); ourNode.Type = ChangeType.Add; ourNode.IsFolder = otherNode.IsFolder; break; case ChangeType.SetDisplayName: if (ourNode.Type == ChangeType.None) ourNode.Type = ChangeType.SetDisplayName; break; case ChangeType.Delete: if (ourNode.Type == ChangeType.None || ourNode.Type == ChangeType.SetDisplayName) ourNode.Type = ChangeType.Delete; break; case ChangeType.Undelete: if (ourNode.Type == ChangeType.Add && ourNode.IsFolder) throw new Exception("Add folder conflicts with Undelete file."); if (ourNode.Type == ChangeType.None || ourNode.Type == ChangeType.SetDisplayName || ourNode.Type == ChangeType.Delete) { ourNode.Type = ChangeType.Undelete; } break; } } else { if (Nodes == null) Nodes = new Dictionary<string, ChangeNode>(); ourNode = otherNode.ShallowClone(this); Nodes.Add(ourNode.Name, ourNode); } if (ourNode.IsFolder && otherNode.IsFolder && ourNode.Type != ChangeType.Delete && otherNode.Type != ChangeType.Delete) { ourNode.PreservingMerge(otherNode); } } }
/// <summary> /// Determines if a preserving merge will succeed. /// /// Note: a.PreservingConflicts(b) is always equal to b.PreservingConflicts(a). /// </summary> /// <param name="other">The other node.</param> /// <param name="callback">A callback invoked for each conflict.</param> public bool PreservingConflicts(ChangeNode other, Action<ChangeNode, ChangeNode> callback = null) { if (Nodes == null || other.Nodes == null) return false; bool conflict = false; foreach (var otherNode in other.Nodes.Values) { ChangeNode ourNode = null; if (!Nodes.TryGetValue(otherNode.Name, out ourNode)) continue; switch (otherNode.Type) { case ChangeType.Add: if (ourNode.Type == ChangeType.Add && (!ourNode.IsFolder || !otherNode.IsFolder)) { conflict = true; if (callback != null) callback(ourNode, otherNode); else return true; } if (ourNode.Type == ChangeType.Undelete && otherNode.IsFolder) { conflict = true; if (callback != null) callback(ourNode, otherNode); else return true; } break; case ChangeType.Undelete: if (ourNode.Type == ChangeType.Add && ourNode.IsFolder) { conflict = true; if (callback != null) callback(ourNode, otherNode); else return true; } break; } if (!conflict && ourNode.IsFolder && otherNode.IsFolder && ourNode.Type != ChangeType.Delete && otherNode.Type != ChangeType.Delete) { if (ourNode.PreservingConflicts(otherNode, callback)) { conflict = true; if (callback == null) return true; } } } return conflict; }
/// <summary> /// Makes <paramref name="other"/> sequential with respect to this node, so that /// <code>this.MakePreserving(other); this.SequentialMerge(other);</code> has the /// same effect as <code>this.PreservingMerge(other)</code>. It is assumed that /// <see cref="PreservingConflicts"/> returns true. /// </summary> /// <param name="other">The other node.</param> /// <param name="callback">A callback invoked for each change removed.</param> public void MakeSequentialByPreserving(ChangeNode other, Action<ChangeNode, ChangeNode> callback = null) { if (other.Nodes == null) return; foreach (var otherNode in other.Nodes.Values) { ChangeNode ourNode = null; if (Nodes != null && Nodes.TryGetValue(otherNode.Name, out ourNode)) { switch (otherNode.Type) { case ChangeType.Add: if (ourNode.Type == ChangeType.Add && (!ourNode.IsFolder || !otherNode.IsFolder)) throw new Exception("Add conflicts with Add."); if (ourNode.Type == ChangeType.Undelete && otherNode.IsFolder) throw new Exception("Add folder conflicts with Undelete file."); break; case ChangeType.SetDisplayName: if (ourNode.Type == ChangeType.Delete) { if (callback != null) callback(ourNode, otherNode); otherNode.Type = ChangeType.None; } break; case ChangeType.Delete: if (ourNode.Type == ChangeType.Add || ourNode.Type == ChangeType.Delete || ourNode.Type == ChangeType.Undelete) { if (callback != null) callback(ourNode, otherNode); otherNode.Type = ChangeType.None; } break; case ChangeType.Undelete: if (ourNode.Type == ChangeType.Add && ourNode.IsFolder) throw new Exception("Add folder conflicts with Undelete file."); if (ourNode.Type == ChangeType.Add || ourNode.Type == ChangeType.Undelete) { if (callback != null) callback(ourNode, otherNode); otherNode.Type = ChangeType.None; } break; } if (ourNode.IsFolder && otherNode.IsFolder && ourNode.Type != ChangeType.Delete && otherNode.Type != ChangeType.Delete) { ourNode.MakeSequentialByPreserving(otherNode, callback); } } } }
public static ChangeNode FromItems(IEnumerable<ChangeItem> list) { ChangeNode root = CreateRoot(); foreach (var item in list) { var components = item.FullName.ToUpperInvariant().Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); ChangeNode currentNode = root; if (components.Length == 0) throw new Exception("Invalid file name '" + item.FullName + "'"); // Find the direct parent to the file. for (int i = 0; i < components.Length - 1; i++) { var name = components[i]; if (currentNode.Nodes.ContainsKey(name)) { currentNode = currentNode.Nodes[name]; if (!currentNode.IsFolder) throw new Exception("Inconsistent change item '" + currentNode.FullName + "'"); } else { ChangeNode newNode = new ChangeNode { Name = name, FullName = currentNode.FullName + "/" + name, Type = ChangeType.None, IsFolder = true, Parent = currentNode }; if (currentNode.Nodes == null) currentNode.Nodes = new Dictionary<string, ChangeNode>(); currentNode.Nodes.Add(newNode.Name, newNode); currentNode = newNode; } if (currentNode.Type == ChangeType.Delete) { currentNode = null; break; } if (currentNode.Nodes == null) currentNode.Nodes = new Dictionary<string, ChangeNode>(); } // Add the node to direct parent. if (currentNode != null) { var name = components[components.Length - 1]; ChangeNode node = null; if (currentNode.Nodes.TryGetValue(name, out node)) { bool inconsistent = false; if (item.IsFolder && item.Type == ChangeType.Add) inconsistent = node.Type != ChangeType.None && node.Type != ChangeType.Add; else inconsistent = node.Type != ChangeType.None; if (node.IsFolder != item.IsFolder) inconsistent = true; if (inconsistent) throw new Exception("Duplicate change item '" + node.FullName + "'."); node.Type = item.Type; if (node.Type == ChangeType.Delete) node.Nodes = null; } else { node = new ChangeNode { Name = name, FullName = currentNode.FullName + "/" + name, Type = item.Type, IsFolder = item.IsFolder, Parent = currentNode }; currentNode.Nodes.Add(node.Name, node); } } } root.PropagateAdd(); return root; }