Exemple #1
0
        /// <summary>
        /// Persists command data as part of a specific folder.
        /// </summary>
        /// <param name="dataFolder">The path for the folder to write to</param>
        /// <param name="data">The data to be written</param>
        public static void WriteData(string dataFolder, CmdData data)
        {
            string dataPath = Path.Combine(dataFolder, $"{data.Sequence}.json");
            string json     = JsonConvert.SerializeObject(data, Formatting.Indented);

            File.WriteAllText(dataPath, json);
        }
        /// <summary>
        /// Persists command data as part of a specific branch.
        /// </summary>
        /// <param name="branch">The branch the data relates to</param>
        /// <param name="data">The data to be written</param>
        internal override void WriteData(Branch branch, CmdData data)
        {
            string json = JsonConvert.SerializeObject(data, Formatting.Indented);

            Database.ExecuteNonQuery(
                $"INSERT INTO [{branch.Id}] (Sequence,Data) " +
                $"VALUES ({data.Sequence}, '{json}')");
        }
Exemple #3
0
        /// <summary>
        /// Creates a new instance of <see cref="MergeHandler"/>
        /// </summary>
        /// <param name="input">The input parameters for the command.</param>
        /// <exception cref="ArgumentNullException">
        /// Undefined value for <paramref name="input"/>input</exception>
        /// <exception cref="ArgumentException">
        /// The supplied input has an unexpected value for <see cref="CmdData.CmdName"/>
        /// </exception>
        public MergeHandler(CmdData input)
        {
            Input = input ?? throw new ArgumentNullException(nameof(Input));

            if (Input.CmdName != nameof(IMerge))
            {
                throw new ArgumentException(nameof(Input.CmdName));
            }
        }
        public CloneStoreHandler(CmdData input)
        {
            Input = input ?? throw new ArgumentNullException();

            if (Input.CmdName != nameof(ICloneStore))
            {
                throw new ArgumentException(nameof(Input.CmdName));
            }
        }
Exemple #5
0
        /// <summary>
        /// Creates a new instance of <see cref="CreateBranchHandler"/>
        /// </summary>
        /// <param name="input">The input parameters for the command.</param>
        /// <exception cref="ArgumentNullException">
        /// Undefined value for <paramref name="input"/></exception>
        /// <exception cref="ArgumentException">
        /// The supplied input has an unexpected value for <see cref="CmdData.CmdName"/>
        /// </exception>
        public CreateBranchHandler(CmdData input)
        {
            Input = input ?? throw new ArgumentNullException(nameof(Input));

            if (Input.CmdName != nameof(ICreateBranch))
            {
                throw new ArgumentException(nameof(Input.CmdName));
            }
        }
        /// <summary>
        /// Persists command data as part of the current branch.
        /// </summary>
        /// <param name="branch">The branch the data relates to</param>
        /// <param name="data">The data to be written</param>
        internal override void WriteData(Branch branch, CmdData data)
        {
            string dataPath = $"{branch.Id}/{data.Sequence}";

            if (Data.ContainsKey(dataPath))
            {
                throw new ApplicationException($"Data already recorded for {dataPath}");
            }

            string json = JsonConvert.SerializeObject(data, Formatting.Indented);

            Data.Add(dataPath, json);
        }
        /// <summary>
        /// Creates a new instance of <see cref="CmdStore"/> that
        /// represents a brand new command store.
        /// </summary>
        /// <param name="storeName">The name for the new store (could be a directory
        /// path if <paramref name="storeType"/> is <see cref="StoreType.File"/>)</param>
        /// <param name="storeType">The type of store to create.</param>
        /// <returns>The newly created command store.</returns>
        public static CmdStore Create(string storeName, StoreType storeType)
        {
            Guid storeId = Guid.NewGuid();

            var c = new CmdData(nameof(ICreateStore), 0, DateTime.UtcNow);

            c.Add(nameof(ICreateStore.StoreId), storeId);
            c.Add(nameof(ICreateStore.Name), storeName);
            c.Add(nameof(ICreateStore.Type), storeType);

            var handler = new CreateStoreHandler(c);
            var ec      = new ExecutionContext();

            handler.Process(ec);
            return(ec.Store);
        }
Exemple #8
0
        /// <summary>
        /// Applies a command to the relevant processors.
        /// </summary>
        /// <param name="data">The data for the command to apply.</param>
        /// <param name="todo">The processors that are relevant to the command.</param>
        /// <returns>The number of processors that successfully
        /// handled the command.</returns>
        void Apply(CmdData data, ICmdProcessor[] todo)
        {
            foreach (ICmdProcessor p in todo)
            {
                try
                {
                    p.Process(data);
                }

                catch (ProcessorException)
                {
                    throw;
                }

                catch (Exception ex)
                {
                    throw new ProcessorException(p, "Apply failed for " + p.GetType().Name, ex);
                }
            }
        }
Exemple #9
0
        /// <summary>
        /// Creates a local branch from this remote branch.
        /// </summary>
        /// <param name="ec">The current execution context</param>
        /// <returns>The newly created branch which the execution context
        /// now refers to.</returns>
        /// <exception cref="ApplicationException">This is not a remote branch,
        /// or the branch already has at least one local child branch.</exception>
        public Branch CreateLocal(ExecutionContext ec)
        {
            if (!IsRemote)
            {
                throw new ApplicationException("Branch is already local");
            }

            if (!CanBranch)
            {
                throw new ApplicationException("A branch received from a clone cannot be branched");
            }

            // Search for a local child
            if (Children.Any(x => !x.IsRemote))
            {
                throw new ApplicationException("Remote branch already has at least one child");
            }

            // Create a new child branch
            var data = new CmdData(
                cmdName: nameof(ICreateBranch),
                sequence: 0,
                createdAt: DateTime.UtcNow);

            // + is a valid folder name, but may well not work in other types of store
            string childName = "+";

            data.Add(nameof(ICreateBranch.Name), childName);
            data.Add(nameof(ICreateBranch.CommandCount), Info.CommandCount);

            var cb = new CreateBranchHandler(data);

            cb.Process(ec);
            Branch result = GetChild(childName);

            // And switch to it
            ec.Store.SwitchTo(result);
            Log.Info($"Created (and switched to) {result}");

            return(result);
        }
        internal static MemoryStore Create(CmdData args)
        {
            // Disallow an attempt to clone another memory store
            // TODO: How should the ICloneStore input reference another memory store?
            if (args.CmdName == nameof(ICloneStore))
            {
                throw new NotImplementedException(nameof(MemoryStore));
            }

            // Create the AC file that represents the store root branch
            Guid   storeId = args.GetGuid(nameof(ICreateStore.StoreId));
            string name    = args.GetValue <string>(nameof(ICreateStore.Name));

            var ac = new BranchInfo(storeId: storeId,
                                    parentId: Guid.Empty,
                                    branchId: storeId,
                                    branchName: name,
                                    createdAt: args.CreatedAt);

            return(new MemoryStore(ac));
        }
Exemple #11
0
        public static CmdStore CreateStore(CmdData args)
        {
            StoreType type = args.GetEnum <StoreType>(nameof(ICreateStore.Type));

            if (type == StoreType.File)
            {
                return(FileStore.Create(args));
            }

            if (type == StoreType.SQLite)
            {
                return(SQLiteStore.Create(args));
            }

            if (type == StoreType.Memory)
            {
                return(MemoryStore.Create(args));
            }

            throw new NotImplementedException(type.ToString());
        }
Exemple #12
0
        /// <summary>
        /// Applies a command to the relevant processors.
        /// </summary>
        /// <param name="data">The data for the command to apply.</param>
        /// <returns>True if the command was applied as expected. False if
        /// an error arose. Any error will be written to the log file. The caller
        /// can also obtain details via the <see cref="LastProcessingError"/>
        /// property.
        /// </returns>
        public bool Apply(CmdData data)
        {
            LastProcessingError = null;
            ICmdProcessor[] todo = Processors.Where(x => x.Filter.IsRelevant(data))
                                   .ToArray();

            try
            {
                Apply(data, todo);
                return(true);
            }

            catch (ProcessorException pex)
            {
                LastProcessingError = pex;
                Log.Error(pex, pex.Message);

                // Attempt to undo anything we've done (up to and
                // including the processor where the failure arose)
                foreach (ICmdProcessor p in todo)
                {
                    try { p.Undo(data); }
                    catch (Exception ex)
                    {
                        Log.Error(ex, "Undo failed for " + p.GetType().Name);
                    }

                    // Break if we have just undone stuff for the processor that failed
                    // (the Apply method breaks on the first failure)
                    if (Object.ReferenceEquals(p, pex.Processor))
                    {
                        break;
                    }
                }
            }

            return(false);
        }
Exemple #13
0
 /// <summary>
 /// Persists command data as part of the current branch.
 /// </summary>
 /// <param name="branch">The branch the data relates to</param>
 /// <param name="data">The data to be written</param>
 /// <remarks>
 /// To be called only by <see cref="Branch.SaveData"/>
 /// </remarks>
 internal abstract void WriteData(Branch branch, CmdData data);
 /// <summary>
 /// Is an instance of <see cref="CmdData"/> relevant?
 /// </summary>
 /// <param name="data">The command to be checked.</param>
 /// <returns>True if the command is relevant (i.e. should not be
 /// filtered out). False if the command can be ignored.</returns>
 public bool IsRelevant(CmdData data)
 {
     return(Names.Contains(data.CmdName));
 }
Exemple #15
0
        /// <summary>
        /// Persists command data as part of a specific branch.
        /// </summary>
        /// <param name="branch">The branch the data relates to</param>
        /// <param name="data">The data to be written</param>
        internal override void WriteData(Branch branch, CmdData data)
        {
            string dataFolder = GetBranchDirectoryName(branch);

            WriteData(dataFolder, data);
        }
Exemple #16
0
        internal static FileStore Create(CmdData args)
        {
            // Expand the supplied name to include the current working directory (or
            // expand a relative path)
            string enteredName = args.GetValue <string>(nameof(ICreateStore.Name));
            string name        = Path.GetFileNameWithoutExtension(enteredName);
            string folderName  = Path.GetFullPath(enteredName);

            // Disallow if the folder name already exists.

            // It may be worth relaxing this rule at some future date. The
            // reason for disallowing it is because an existing folder may
            // well contain sub-folders, but we also use sub-folders to
            // represent the branch hierarchy. So things would be a bit
            // mixed up. That said, it would be perfectly plausible to
            // place branch sub-folders under a separate ".ac" folder (in
            // much the same way as git). That might be worth considering
            // if we want to support a "working directory" like that
            // provided by git.

            if (Directory.Exists(folderName))
            {
                throw new ApplicationException($"{folderName} already exists");
            }

            // Confirm the folder is on a local drive
            if (!IsLocalDrive(folderName))
            {
                throw new ApplicationException("Command stores can only be initialized on a local fixed drive");
            }

            // Create the folder for the store (but if the folder already exists,
            // confirm that it does not already hold any AC files).
            if (Directory.Exists(folderName))
            {
                if (GetAcFilePath(folderName) != null)
                {
                    throw new ApplicationException($"{folderName} has already been initialized");
                }
            }
            else
            {
                Log.Info("Creating " + folderName);
                Directory.CreateDirectory(folderName);
            }

            Guid      storeId = args.GetGuid(nameof(ICreateStore.StoreId));
            FileStore result  = null;

            // If we're cloning, copy over the source data
            if (args.CmdName == nameof(ICloneStore))
            {
                // TODO: When cloning from a real remote, using wget might be a
                // good choice (but that doesn't really fit with what's below)

                ICloneStore  cs = (args as ICloneStore);
                IRemoteStore rs = GetRemoteStore(cs);

                // Retrieve metadata for all remote branches
                BranchInfo[] acs = rs.GetBranches()
                                   .OrderBy(x => x.CreatedAt)
                                   .ToArray();

                var branchFolders = new Dictionary <Guid, string>();

                // Copy over all branches
                foreach (BranchInfo ac in acs)
                {
                    // Get the data for the branch (do this before we do any tweaking
                    // of the folder path from the AC file -- if the command supplier is
                    // a local file store, we won't be able to read the command data files
                    // after changing the folder name recorded in the AC)
                    IdRange   range = new IdRange(ac.BranchId, 0, ac.CommandCount - 1);
                    CmdData[] data  = rs.GetData(range).ToArray();

                    // TODO: what follows is very similar to the CopyIn method.
                    // Consider using that instead (means an empty FileStore needs
                    // to be created up front)

                    // Determine the output location for the AC file (relative to the
                    // location that should have already been defined for the parent)

                    if (!branchFolders.TryGetValue(ac.ParentId, out string parentFolder))
                    {
                        Debug.Assert(ac.ParentId.Equals(Guid.Empty));
                        parentFolder = folderName;
                    }

                    string dataFolder = Path.Combine(parentFolder, ac.BranchName);
                    branchFolders[ac.BranchId] = dataFolder;
                    SaveBranchInfo(dataFolder, ac);

                    // Copy over the command data
                    foreach (CmdData cd in data)
                    {
                        FileStore.WriteData(dataFolder, cd);
                    }
                }

                // If the origin is a folder on the local file system, ensure it's
                // saved as an absolute path (relative specs may confuse directory
                // navigation, depending on what the current directory is at the time)

                string origin = cs.Origin;
                if (Directory.Exists(origin))
                {
                    origin = Path.GetFullPath(origin);
                }

                // Save the store metadata
                var root = new StoreInfo(storeId, name, rs.Id, origin);
                SaveStoreInfo(folderName, root);

                // And suck it back up again
                string acSpec = Path.Combine(folderName, acs[0].BranchId + ".ac");
                result = FileStore.Load(acSpec);
            }
            else
            {
                // Create the AC file that represents the store root branch
                var ac = new BranchInfo(storeId: storeId,
                                        parentId: Guid.Empty,
                                        branchId: storeId,
                                        branchName: name,
                                        createdAt: args.CreatedAt);

                // Create the store metadata
                var storeInfo = new StoreInfo(storeId, name, Guid.Empty);

                // Create the store and save it
                result = new FileStore(folderName,
                                       storeInfo,
                                       new BranchInfo[] { ac },
                                       ac.BranchId);

                // Save the AC file plus the store metadata
                FileStore.SaveBranchInfo(folderName, ac);
                result.SaveStoreInfo();
            }

            return(result);
        }
Exemple #17
0
 /// <summary>
 /// Creates a new instance of <see cref="Cmd"/>
 /// that is not linked to any other command.
 /// </summary>
 /// <param name="branch">The branch that the command is part of (not null).</param>
 /// <param name="data">The data for the command (not null).</param>
 /// <exception cref="ArgumentNullException">One of the supplied parameters
 /// is undefined.</exception>
 public Cmd(Branch branch, CmdData data)
 {
     Branch     = branch ?? throw new ArgumentNullException(nameof(Branch));
     Data       = data ?? throw new ArgumentNullException(nameof(Data));
     References = null;
 }
        internal static SQLiteStore Create(CmdData args)
        {
            // Disallow store names that correspond to tables in the database (bear
            // in mind that the entered name may or may not include a directory path)
            string enteredName = args.GetValue <string>(nameof(ICreateStore.Name));
            string name        = Path.GetFileNameWithoutExtension(enteredName);

            if (IsReservedName(name))
            {
                throw new ApplicationException("Store name not allowed");
            }

            // Expand the supplied name to include the current working directory (or
            // expand a relative path)
            string fullSpec = Path.GetFullPath(enteredName);
            string fileType = Path.GetExtension(fullSpec);

            if (String.IsNullOrEmpty(fileType))
            {
                fullSpec += ".ac-sqlite";
            }

            // Disallow if the database file already exists
            if (File.Exists(fullSpec))
            {
                throw new ApplicationException($"{fullSpec} already exists");
            }

            // Confirm the file is on a local drive
            if (!IsLocalDrive(fullSpec))
            {
                throw new ApplicationException("Command stores can only be initialized on a local fixed drive");
            }

            // Ensure the folder exists
            string folderName = Path.GetDirectoryName(fullSpec);

            if (!Directory.Exists(folderName))
            {
                Log.Trace("Creating " + folderName);
                Directory.CreateDirectory(folderName);
            }

            Guid        storeId = args.GetGuid(nameof(ICreateStore.StoreId));
            SQLiteStore result  = null;

            if (args.CmdName == nameof(ICloneStore))
            {
                // Copy the SQLite database
                // TODO: To handle database files on remote machines
                ICloneStore cs = (args as ICloneStore);
                Log.Info($"Copying {cs.Origin} to {fullSpec}");
                File.Copy(cs.Origin, fullSpec);

                // Load the copied store
                result = SQLiteStore.Load(fullSpec, cs);
            }
            else
            {
                Log.Info("Creating " + fullSpec);
                SQLiteDatabase db = CreateDatabase(fullSpec);

                // Create the AC file that represents the store root branch
                var ac = new BranchInfo(storeId: storeId,
                                        parentId: Guid.Empty,
                                        branchId: storeId,
                                        branchName: name,
                                        createdAt: args.CreatedAt);

                // Create the store metadata
                var storeInfo = new StoreInfo(storeId, name, Guid.Empty);

                // Create the store and save it
                result = new SQLiteStore(storeInfo,
                                         new BranchInfo[] { ac },
                                         ac.BranchId,
                                         db);

                // Save the info for the master branch plus the store metadata
                result.SaveBranchInfo(ac);
                result.SaveStoreInfo();

                // The last branch is the root of the new database
                result.SaveProperty(PropertyNaming.LastBranch, storeId.ToString());
            }

            return(result);
        }
Exemple #19
0
        /// <summary>
        /// Saves command data relating to this branch.
        /// </summary>
        /// <param name="data">The data to be written.</param>
        /// <exception cref="ApplicationException">Attempt to save new data
        /// to a branch imported from a remote store.</exception>
        /// <exception cref="ArgumentException">The supplied data does not
        /// have a sequence number that comes at the end of this branch.
        /// </exception>
        /// <remarks>
        /// This method can be used only to append data to a branch was created
        /// as part of the local store. As well as saving the command data, it
        /// will mutate the AC metadata for the branch.
        /// <para/>
        /// TODO: At the present time, a merge from a child branch will also
        /// mutate the AC metadata for the child. This is bad because the
        /// child could be a remote, but it would be wise to disallow any
        /// attempt to mutate anything relating to a remote branch.
        /// </remarks>
        public void SaveData(CmdData data)
        {
            // Make some last-minute checks (these should have been done already)

            if (IsRemote)
            {
                throw new ApplicationException("Attempt to mutate remote branch");
            }

            if (Info.IsCompleted)
            {
                throw new ApplicationException("Attempt to mutate a completed branch");
            }

            // The data must come at the end of the current branch
            if (data.Sequence != Info.CommandCount)
            {
                throw new ArgumentException(
                          $"Unexpected command sequence {data.Sequence} " +
                          $"(should be {Info.CommandCount})");
            }

            // Persist the command data
            Store.WriteData(this, data);

            // And remember it as part of this branch
            Commands.Add(data);

            // If the command is being appended to the current branch,
            // ensure it's also included in the stream (the command may
            // not be in the current branch because we may be creating
            // a new branch)

            // TODO: The Stream might not be defined when creating a new store

            if (this.Equals(Store.Current))
            {
                Store.Stream?.Cmds.AddLast(new Cmd(this, data));
            }

            // Update the AC file to reflect the latest command
            Info.CommandCount = data.Sequence + 1;
            Info.UpdatedAt    = data.CreatedAt;

            // Update the appropriate merge count if we've just done a merge
            if (data.CmdName == nameof(IMerge))
            {
                IMerge m      = (data as IMerge);
                Guid   fromId = m.FromId;
                uint   numCmd = m.MaxCmd + 1;

                if (fromId.Equals(Info.ParentId))
                {
                    // Increment the number of merges from the parent
                    this.Info.CommandDiscount++;

                    // Just done a merge from the parent (this is the child,
                    // now matches the parent)
                    this.Info.RefreshCount = Parent.Info.CommandCount;

                    // Record the number of times that the parent has already merged
                    // from the child (if at all)
                    uint parentDiscount = 0;
                    if (Parent.Info.LastMerge.TryGetValue(this.Id, out MergeInfo mi))
                    {
                        parentDiscount = mi.ParentDiscount;
                    }

                    this.Info.RefreshDiscount = parentDiscount;
                }
                else
                {
                    // Merge is from a child.
                    Branch child = Children.FirstOrDefault(x => x.Id.Equals(fromId));
                    if (child == null)
                    {
                        throw new ApplicationException($"Cannot locate child {fromId}");
                    }

                    // The child doesn't need to consider the merge that the parent has done,
                    // so increment the number of parent commands the child can ignore

                    if (Info.LastMerge.TryGetValue(fromId, out MergeInfo mi))
                    {
                        Info.LastMerge[fromId] = new MergeInfo(numCmd,
                                                               child.Info.CommandDiscount,
                                                               mi.ParentDiscount + 1);
                    }
                    else
                    {
                        Info.LastMerge.Add(fromId, new MergeInfo(numCmd, child.Info.CommandDiscount, 1));
                    }
                }

                // Reload the stream
                // TODO: Doing it from scratch is perhaps a bit heavy-handed,
                // is there a more efficient way to do it?
                Store.Stream = CreateStream();
            }

            // Save the mutated branch metadata
            Store.SaveBranchInfo(this);
        }