Example #1
0
        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;
        }
Example #2
0
        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;
                }
            }
        }
Example #3
0
 public ChangeNode ShallowClone(ChangeNode newParent)
 {
     return new ChangeNode { Name = Name, FullName = FullName, Type = Type, IsFolder = IsFolder, Parent = newParent };
 }
Example #4
0
        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);
        }
Example #5
0
        /// <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);
            }
        }
Example #6
0
        /// <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);
                }
            }
        }
Example #7
0
        /// <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;
        }
Example #8
0
        /// <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);
                    }
                }
            }
        }
Example #9
0
        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;
        }