/// <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}')"); }
/// <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)); } }
/// <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); }
/// <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); } } }
/// <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)); }
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()); }
/// <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); }
/// <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)); }
/// <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); }
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); }
/// <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); }
/// <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); }