/// <summary> /// Saves the metadata for a branch that is part of this store. /// </summary> /// <param name="folderName">The path for the folder that holds branch data.</param> /// <param name="ac">The branch metadata to be saved.</param> internal static void SaveBranchInfo(string folderName, BranchInfo ac) { Directory.CreateDirectory(folderName); string data = JsonConvert.SerializeObject(ac, Formatting.Indented); string fileName = Path.Combine(folderName, ac.BranchId + ".ac"); File.WriteAllText(fileName, data); }
/// <summary> /// Creates a new instance of <see cref="Branch"/> (but does /// not load it). /// </summary> /// <param name="store">The command store that this branch is part of (not null)</param> /// <param name="ac">Metadata relating to the branch (not null)</param> /// <exception cref="ArgumentNullException"> /// The supplied command store or metadata is undefined</exception> internal Branch(CmdStore store, BranchInfo ac) { Store = store ?? throw new ArgumentNullException(nameof(store)); Info = ac ?? throw new ArgumentNullException(nameof(ac)); Commands = new List <CmdData>(); Parent = null; Children = new List <Branch>(); }
/// <summary> /// Accepts data from another command store. /// </summary> /// <param name="source">A name that identifies the command store that is /// the source of the data.</param> /// <param name="ac">The metadata for the branch the commands are part of.</param> /// <param name="data">The command data to be appended to the remote branch.</param> void IRemoteStore.Push(string source, BranchInfo ac, CmdData[] data) { // Clone the supplied metadata (if the call actually comes from // the current application, we don't want to mutate the metadata) BranchInfo acCopy = ac.CreateCopy(); string altName = acCopy.BranchName == "+" ? source : null; CopyIn(acCopy, data, altName); }
/// <summary> /// Performs the data processing associated with a command. /// </summary> public void Process(ExecutionContext context) { CmdStore cs = context?.Store ?? throw new ApplicationException("Undefined store"); uint numCmd = (Input as ICreateBranch).CommandCount; if (numCmd == 0) { throw new ApplicationException(nameof(numCmd)); } // Confirm that the name for the new branch is not a // duplicate (considering just the children of the // current branch) string name = (Input as ICreateBranch).Name; Branch parent = cs.Current; Branch oldBranch = parent.GetChild(name); if (oldBranch != null) { throw new ArgumentException( $"Branch {name} previously created at {oldBranch.Info.CreatedAt}"); } // Confirm that the new branch name is acceptable to the command store if (!cs.IsValidBranchName(name)) { throw new ArgumentException($"Branch name '{name}' is not allowed"); } var ac = new BranchInfo( storeId: cs.Id, parentId: parent.Id, branchId: Guid.NewGuid(), branchName: name, createdAt: Input.CreatedAt, updatedAt: Input.CreatedAt, commandDiscount: 1, refreshCount: numCmd); // Update internal structure to include the new branch var newBranch = new Branch(cs, ac) { Parent = parent }; parent.Children.Add(newBranch); cs.Branches.Add(ac.BranchId, newBranch); // Save the metadata for the new branch cs.SaveBranchInfo(newBranch); // And save the CreateBranch command itself newBranch.SaveData(Input); }
/// <summary> /// Initializes a new instance of the <see cref="MemoryStore"/> class. /// </summary> /// <param name="rootAc">Metadata for the root branch</param> MemoryStore(BranchInfo rootAc) : base(String.Empty, new StoreInfo(rootAc), new BranchInfo[] { rootAc }, rootAc.BranchId) { AcFiles = new Dictionary <Guid, string>(); Data = new Dictionary <string, string>(); SaveBranchInfo(rootAc); }
/// <summary> /// Prepares an instance of <see cref="CmdData"/> for /// the next command that will be appended to the /// current branch. /// </summary> /// <param name="commandName">A name that identifies /// the command type (a class name is one of potentially /// many choices)</param> /// <returns>An instance of command data that can /// be appended to the current branch.</returns> /// <remarks> /// The caller will usually append additional properties /// that are specific to the command. /// </remarks> public CmdData CreateCmdData(string commandName) { BranchInfo ac = Store.Current.Info; // Never return command data for a completed branch if (ac.IsCompleted) { throw new InvalidOperationException("Branch has already been completed"); } // And never for a remote // May be doing a push (which you can do from any branch) //if (Store.Current.IsRemote) // throw new InvalidOperationException("Unexpected attempt to append to a remote branch"); return(new CmdData( cmdName: commandName, sequence: ac.CommandCount, createdAt: DateTime.UtcNow)); }
void SaveBranchInfo(BranchInfo ac) { string data = JsonConvert.SerializeObject(ac, Formatting.Indented); string sql; int nRows = 0; if (ac.CommandCount == 0) { Database.ExecuteTransaction(() => { // Create the data table for the new branch sql = $"CREATE TABLE [{ac.BranchId}] " + "(Sequence INTEGER NOT NULL PRIMARY KEY" + ",Data JSON NOT NULL)"; Database.ExecuteNonQuery(sql); // Create a view with a matching name (this is not needed by the // software, but may make it easier to debug things) string viewName = GetDataViewName(ac.BranchName); sql = $"CREATE VIEW [{viewName}] AS SELECT * FROM [{ac.BranchId}]"; Database.ExecuteNonQuery(sql); // Record branch metadata sql = $"INSERT INTO Branches (Id,Name,CreatedAt,Data) VALUES " + $"('{ac.BranchId}', '{viewName}', '{ac.CreatedAt:o}', '{data}')"; nRows = Database.ExecuteNonQuery(sql); }); } else { sql = $"UPDATE Branches SET Data='{data}' WHERE Id='{ac.BranchId}'"; nRows = Database.ExecuteNonQuery(sql); } if (nRows != 1) { throw new ApplicationException($"Update for branch changed {nRows} row`s".TrimExtras()); } }
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 void Process(ExecutionContext context) { CmdStore cs = context.Store; StoreInfo storeInfo = cs.Store; string upLoc = storeInfo.UpstreamLocation; if (String.IsNullOrEmpty(upLoc)) { throw new ApplicationException("There is no upstream location"); } // Collate how much we already have in all remote branches. // We may have previously pushed some local branches to the remote // store, but nothing is supposed to mutate those remote copies. IdCount[] have = cs.Branches.Values .Where(x => x.IsRemote) .Select(x => new IdCount(x.Id, x.Info.CommandCount)) .ToArray(); // Open a channel to the upstream store IRemoteStore rs = context.GetRemoteStore(upLoc); // Determine what we are missing (including any new branches in the remote) IdRange[] toFetch = rs.GetMissingRanges(cs.Id, have, true); // How many commands do we need to fetch uint total = (uint)toFetch.Sum(x => x.Size); Log.Info($"To fetch {total} command`s from {toFetch.Length} branch`es".TrimExtras()); // Retrieve the command data from the remote, keeping new branches // apart from appends to existing branches. var newBranchData = new Dictionary <BranchInfo, CmdData[]>(); var moreBranchData = new Dictionary <BranchInfo, CmdData[]>(); foreach (IdRange idr in toFetch) { // Fetch the remote AC file BranchInfo ac = rs.GetBranchInfo(idr.Id); if (ac == null) { throw new ApplicationException("Could not locate remote branch " + idr.Id); } // And the relevant data Log.Info($"Fetch [{idr.Min},{idr.Max}] for {ac.BranchName} ({ac.BranchId})"); CmdData[] branchData = rs.GetData(idr).ToArray(); if (cs.FindBranch(ac.BranchId) == null) { newBranchData.Add(ac, branchData); } else { moreBranchData.Add(ac, branchData); } } // All done with the remote store // Copy any brand new branches (ensuring they get created in the // right order so that parent/child relationships can be formed // as we go). foreach (KeyValuePair <BranchInfo, CmdData[]> kvp in newBranchData.OrderBy(x => x.Key.CreatedAt)) { cs.CopyIn(kvp.Key, kvp.Value); } // Append command data for branches we previously had (the order // shouldn't matter) foreach (KeyValuePair <BranchInfo, CmdData[]> kvp in moreBranchData) { cs.CopyIn(kvp.Key, kvp.Value); } Log.Info("Fetch completed"); // Reload the current command stream (from scratch, kind of brute force, // not sure whether appending the new commands would really be sufficient) // TODO: Is this really necessary? Perhaps only if the current branch has // been fetched (the stuff we're fetching should only come from remotes, // but the current branch could be one of those remotes) //cs.Stream = new CmdStream(cs.Current); }
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> /// Copies in data from a remote branch. /// </summary> /// <param name="ac">The branch metadata received from a remote store /// (this will be used to replace any metadata already held locally).</param> /// <param name="data">The command data to append to the local branch.</param> /// <param name="altName">An alternative name to assign to the branch.</param> /// <exception cref="ApplicationException"> /// Attempt to import remote data into a local branch</exception> public override void CopyIn(BranchInfo ac, CmdData[] data, string altName = null) { // The incoming data can only come from a remote store Debug.Assert(!ac.StoreId.Equals(this.Id)); // Define the local location for the AC file (relative to the // location that should have already been defined for the parent). // There could be no parent if we're copying in command data from // the root branch. string dataFolder; Branch parent; if (ac.ParentId.Equals(Guid.Empty)) { parent = null; dataFolder = RootDirectoryName; } else { parent = FindBranch(ac.ParentId); if (parent == null) { throw new ApplicationException("Cannot find parent branch " + ac.ParentId); } string parentDir = GetBranchDirectoryName(parent); dataFolder = Path.Combine(parentDir, altName ?? ac.BranchName); } // Save the supplied AC in its new location (if the branch already // exists locally, this will overwrite the AC) SaveBranchInfo(dataFolder, ac); Branch branch = FindBranch(ac.BranchId); if (branch == null) { Log.Trace($"Copying {data.Length} commands to {dataFolder}"); Debug.Assert(data[0].Sequence == 0); Debug.Assert(data[0].CmdName == nameof(ICreateBranch)); Debug.Assert(parent != null); // Create the new branch branch = new Branch(this, ac) { Parent = parent }; parent.Children.Add(branch); Branches.Add(ac.BranchId, branch); } else { Log.Trace($"Appending {data.Length} commands to {dataFolder}"); if (!branch.IsRemote) { throw new ApplicationException("Attempt to import remote data into a local branch"); } // Replace the cached metadata branch.Info = ac; } // Copy over the command data foreach (CmdData cd in data) { WriteData(dataFolder, cd); } }
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 the metadata for a branch that is part of this store. /// </summary> /// <param name="ac">The branch metadata to be saved.</param> void SaveBranchInfo(BranchInfo ac) { string data = JsonConvert.SerializeObject(ac, Formatting.Indented); AcFiles[ac.BranchId] = data; }
/// <summary> /// Copies in data from a new remote branch. /// </summary> /// <param name="ac">The branch metadata received from a remote store.</param> /// <param name="data">The command data for the branch</param> /// <param name="altName">An alternative name to assign to the branch.</param> public virtual void CopyIn(BranchInfo ac, CmdData[] data, string altName = null) { // Currently implemented only by FileStore throw new NotImplementedException(); }
/// <summary> /// Creates a new instance of <see cref="StoreInfo"/> for /// a brand new command store. /// </summary> /// <param name="ac">The branch metadata for the root branch</param> internal StoreInfo(BranchInfo ac) : this(ac.StoreId, ac.BranchName, Guid.Empty) { }