public override SoundboxNode CompareCopy() { var other = new SoundboxDirectory(); CompareCopyFill(other); return(other); }
/// <summary> /// Returns the given directory with its content from our database. Pass null to get the root directory. /// </summary> /// <param name="directory"></param> /// <returns></returns> //TODO we've got a bit of a race condition here: internal access is synchronized (mostly, see usage of GetCleanFile). // however we can get into trouble when returning files either via public getters or via client events: files can change after they have been returned here, so while the serializer is running. // to fully fix this we'd have to completely copy the entire sound tree whenever something changes. returning copies only would mostly work; we'd have to make sure that internally everything is synchronized public Task <SoundboxDirectory> GetDirectory(SoundboxDirectory directory = null) { if (directory == null) { return(Task.FromResult(GetRootDirectory())); } return(Task.FromResult(GetCleanFile(directory))); }
/// <summary> /// Sets the given watermark on a directory and all of its ancestors. This is required whenever this directory or its content is changed. /// </summary> /// <param name="directory"></param> /// <param name="watermark"></param> protected void SetWatermark(SoundboxDirectory directory, Guid watermark) { do { directory.Watermark = watermark; //TODO async Database.Update(directory); directory = directory.ParentDirectory; } while (directory != null); }
/// <summary> /// /// </summary> /// <param name="status"></param> /// <param name="file"></param> /// <param name="previousWatermark"></param> /// <param name="fromDirectory"> /// See <see cref="SoundboxFileMoveEvent.FromDirectory"/> /// </param> public FileResult(ResultStatus status, SoundboxNode file, Guid?previousWatermark, SoundboxDirectory fromDirectory = null) : base(status) { if (file != null) { File = file.Flatten(true); } PreviousWatermark = previousWatermark; if (fromDirectory != null) { FromDirectory = fromDirectory.Flatten() as SoundboxDirectory; } }
/// <summary> /// Recursively adds the directory, all nodes in the directory and its descendant nodes to <see cref="NodesCache"/>. /// </summary> /// <param name="directory"></param> protected void BuildNodeCache(SoundboxDirectory directory) { NodesCache[directory.ID] = directory; foreach (var child in directory.Children) { NodesCache[child.ID] = child; if (child is SoundboxDirectory childDirectory) { BuildNodeCache(childDirectory); } } }
/// <summary> /// Creates a new directory in the given parent directory. /// </summary> /// <param name="directory"> /// Information used when adding the new sound:<list type="bullet"> /// <item><see cref="SoundboxNode.Name"/></item> /// <item><see cref="SoundboxNode.Tags"/></item> /// </list> /// </param> /// <param name="parent"> /// Null: root directory is assumed. /// </param> /// <returns></returns> public FileResult MakeDirectory(SoundboxDirectory directory, SoundboxDirectory parent) { if (directory == null) { //error return(new FileResult(BaseResultStatus.INVALID_PARAMETER)); } if (!CheckUploadDisplayName(directory.Name)) { //error return(new FileResult(FileResultStatus.INVALID_FILE_NAME)); } if (parent == null) { parent = GetRootDirectory(); } else { parent = GetCleanFile(parent); if (parent == null) { //given parent does not exist => error return(new FileResult(FileResultStatus.FILE_DOES_NOT_EXIST)); } } //TODO check display name unique in parent directory.ID = Guid.NewGuid(); directory.Children = new List <SoundboxNode>(); directory.ParentDirectory = parent; ResultStatus status = BaseResultStatus.INTERNAL_SERVER_ERROR; try { DatabaseLock.EnterWriteLock(); return(UploadOnNewFile(directory, parent)); } catch (Exception ex) { Log(ex); } finally { DatabaseLock.ExitWriteLock(); } return(new FileResult(status)); }
/// <summary> /// Loads the root directory and thuse the entire file tree from our persistent database. /// If the database is empty then a newly created empty directory is returned. /// </summary> /// <returns></returns> protected async Task <SoundboxDirectory> LoadRoot() { var root = await Database.Get(); if (root == null) { //database is empty => create new root directory root = new SoundboxDirectory(); root.ID = Guid.NewGuid(); root.Watermark = Guid.NewGuid(); await Database.Insert(root); } return(root); }
/// <summary> /// Returns the given directory's children (i.e. fetches <see cref="SoundboxDirectory.Children"/>). /// </summary> /// <param name="directory">The directory content to fetch. Null for base directory</param> /// <param name="recursive">Whether to recursively fetch the entire file branch.</param> /// <returns> /// The directory's content. If null is passed for the <paramref name="directory"/> (requesting the root directory) and <paramref name="recursive"/> is false /// then only the root directory is returned without its children. That can be utilized to quickly verify whether the local sound list is up-to-date by checking the root directory's <see cref="SoundboxDirectory.Watermark"/>. /// </returns> public async Task <ICollection <SoundboxNode> > GetSounds(SoundboxDirectory directory = null, bool recursive = false) { SoundboxDirectory serverDirectory = await GetSoundbox().GetDirectory(directory); if (directory == null && recursive) { //return the entire tree return(new SoundboxNode[] { serverDirectory }); } if (directory == null) { //root directory without children return(new SoundboxNode[] { serverDirectory.Flatten() }); } //return the requested directory's children if (serverDirectory == null) { //invalid directory has been passed return(null); } if (recursive) { return(serverDirectory.Children); } //else: flatten the result; do not return the children's children ICollection <SoundboxNode> children = new List <SoundboxNode>(); foreach (var child in serverDirectory.Children) { children.Add(child.Flatten()); } return(children); }
/// <summary> /// Recursively loads the entire directory's content. /// The given directory itself is loaded and modified as required (in/out parameter). /// </summary> /// <param name="collection"></param> /// <param name="directory"></param> private void LoadDirectory(ILiteCollection <SoundboxNode> collection, SoundboxDirectory directory) { //at this point we already have the children's IDs loaded //still it's probably most efficient to just load the directory's children again var resultChildren = collection.Query() .Include(BsonExpression.Create("children")) .Where(f => f.ID == directory.ID) .Select(BsonExpression.Create("children")); ICollection <SoundboxNode> children = new List <SoundboxNode>(); foreach (var childRow in resultChildren.ToEnumerable()) { if (childRow == null) { continue; } var bsonChildren = childRow["children"]; if (bsonChildren == null) { continue; } foreach (var bsonChild in bsonChildren.AsArray) { var child = this.BsonMapper.Deserialize <SoundboxNode>(bsonChild); //test for dangling references: if (child.ID == default) { //not a proper file continue; } children.Add(child); } } directory.Children = children; //continue loading LoadDirectoryChildren(collection, directory); }
/// <summary> /// Flattens the directory and returns a copy without <see cref="Children"/> and <see cref="SoundboxNode.ParentDirectory"/>. /// This is often used when returning files to a client when only a restricted set of information should be passed instead of the entire file tree. /// </summary> /// <returns></returns> public override SoundboxNode Flatten(bool withParent = false) { var flattened = new SoundboxDirectory() { ID = this.ID, Name = this.Name, IconUrl = this.IconUrl, Tags = this.Tags, Watermark = this.Watermark, Children = null, ParentDirectory = null, Flattened = true }; if (withParent && this.ParentDirectory != null) { flattened.ParentDirectory = this.ParentDirectory.Flatten() as SoundboxDirectory; } return(flattened); }
public async Task <FileResult> Post([FromQuery] string sound, [FromQuery] string directory = null) { Sound soundActual = null; if (!string.IsNullOrWhiteSpace(sound)) { soundActual = JsonConvert.DeserializeObject <Sound>(sound); soundActual.ID = SoundboxNode.ID_DEFAULT_NEW_ITEM; } SoundboxDirectory directoryActual = null; if (!string.IsNullOrWhiteSpace(directory)) { directoryActual = JsonConvert.DeserializeObject <SoundboxDirectory>(directory); } var soundbox = HttpContext.RequestServices.GetService(typeof(Soundbox)) as Soundbox; return(await soundbox.UploadSound(Request.Body, soundActual, directoryActual)); }
/// <summary> /// Adds the given new file to our in-memory data structures and to our database. Updates all clients on success. /// </summary> /// <param name="newFile"></param> /// <param name="parent"></param> private FileResult UploadOnNewFile(SoundboxNode newFile, SoundboxDirectory parent) { try { DatabaseLock.EnterWriteLock(); //save previous watermark for event Guid previousWatermark = GetRootWatermark(); Guid newWatermark = Guid.NewGuid(); //add to cache parent.AddChild(newFile); NodesCache[newFile.ID] = newFile; //add to database //TODO async Database.Insert(newFile); //update cache and database watermarks (this will call Update for parent) SetWatermark(newFile, newWatermark); //update our clients GetHub().OnFileEvent(new SoundboxFileChangeEvent() { Event = SoundboxFileChangeEvent.Type.ADDED, File = FlattenForEvent(newFile), PreviousWatermark = previousWatermark }); if (newFile is Sound sound) { SpeechRecognition_OnSoundChanged(sound, null); } return(new FileResult(BaseResultStatus.OK, newFile, previousWatermark)); } finally { DatabaseLock.ExitWriteLock(); } }
/// <summary> /// Iterates through all of the directory's <see cref="SoundboxDirectory.Children"/> /// and loads the content of child directories via <see cref="LoadDirectory(ILiteCollection{SoundboxNode}, SoundboxDirectory)"/>. /// </summary> /// <param name="collection"></param> /// <param name="directory"></param> private void LoadDirectoryChildren(ILiteCollection <SoundboxNode> collection, SoundboxDirectory directory) { foreach (var child in directory.Children) { child.ParentDirectory = directory; if (child is SoundboxDirectory childDirectory) { LoadDirectory(collection, childDirectory); } } }
/// <summary> /// Moves a file to a new directory without performing an <see cref="Edit(SoundboxNode)"/>. /// </summary> /// <param name="file"></param> /// <param name="directory"> /// Null: uses the root directory instead. /// </param> /// <returns></returns> public async Task <FileResult> Move(SoundboxNode file, SoundboxDirectory directory) { file = GetCleanFile(file); if (file == null) { return(new FileResult(BaseResultStatus.INVALID_PARAMETER)); } if (IsRootDirectory(file)) { return(new FileResult(FileResultStatus.ILLEGAL_FILE_EDIT_DENIED_ROOT)); } if (directory == null) { directory = GetRootDirectory(); } else { directory = GetCleanFile(directory); if (directory == null) { return(new FileResult(BaseResultStatus.INVALID_PARAMETER)); } } if (file == directory) { //not possible return(new FileResult(FileResultStatus.MOVE_TARGET_INVALID)); } if (file.ParentDirectory == directory) { //no change return(new FileResult(BaseResultStatus.OK_NO_CHANGE)); } try { DatabaseLock.EnterWriteLock(); //save previous watermark for event Guid previousWatermark = GetRootWatermark(); Guid newWatermark = Guid.NewGuid(); //move in cache SoundboxDirectory oldParent = file.ParentDirectory; oldParent.Children.Remove(file); directory.AddChild(file); //update file in database if (!(file is SoundboxDirectory)) { //TODO async Database.Update(file); } //else: is updated anyways in SetWatermak //update cache and database watermarks (this will call Update for parent) SetWatermark(file, newWatermark); SetWatermark(oldParent, newWatermark); //update our clients GetHub().OnFileEvent(new SoundboxFileMoveEvent() { Event = SoundboxFileChangeEvent.Type.MOVED, File = FlattenForEvent(file), FromDirectory = oldParent, PreviousWatermark = previousWatermark }); return(new FileResult(BaseResultStatus.OK, file, previousWatermark, oldParent)); } catch (Exception ex) { Log(ex); return(new FileResult(BaseResultStatus.INTERNAL_SERVER_ERROR)); } finally { DatabaseLock.ExitWriteLock(); } }
/// <summary> /// Uploads a new sound and adds it to the given directory. /// </summary> /// <param name="bytes"></param> /// <param name="sound"> /// Information used when adding the new sound:<list type="bullet"> /// <item><see cref="SoundboxNode.Name"/></item> /// <item><see cref="SoundboxFile.FileName"/></item> /// <item><see cref="SoundboxNode.Tags"/></item> /// <item><see cref="Sound.VoiceActivation"/></item> /// </list> /// </param> /// <param name="directory"> /// Null: the root directory is used instead. /// </param> /// <returns></returns> public async Task <FileResult> UploadSound(Stream bytes, Sound sound, SoundboxDirectory directory) { if (sound == null || bytes == null) { //error return(new FileResult(BaseResultStatus.INVALID_PARAMETER)); } if (!CheckUploadFileName(sound.FileName)) { if (GetFileType(sound.FileName) != null && !CheckUploadFileType(sound.FileName)) { //file type is not supported return(new FileResult(FileResultStatus.ILLEGAL_FILE_TYPE)); } return(new FileResult(FileResultStatus.INVALID_FILE_NAME)); } if (!CheckUploadDisplayName(sound.Name)) { //error return(new FileResult(FileResultStatus.INVALID_FILE_NAME)); } if (directory == null) { directory = GetRootDirectory(); } else { directory = GetCleanFile(directory); if (directory == null) { //does not exist return(new FileResult(FileResultStatus.FILE_DOES_NOT_EXIST)); } } //TODO check display name unique in parent //we store only a few select properties of the given sound Sound soundClean = new Sound(); soundClean.ID = Guid.NewGuid(); soundClean.Name = sound.Name; soundClean.FileName = sound.FileName; soundClean.Tags = sound.Tags; soundClean.VoiceActivation = sound.VoiceActivation; soundClean.ParentDirectory = directory; sound = soundClean; MakeSoundFileName(sound); ResultStatus status = BaseResultStatus.INTERNAL_SERVER_ERROR; //TODO: write into temp file, lock database etc, copy file, add to directory object, save database, update clients, unlock //write into temp file string tempFile = GetUploadTempFile(); try { try { using (var output = File.OpenWrite(tempFile)) { await bytes.CopyToAsync(output); } } catch { status = FileResultStatus.IO_ERROR; throw; } //read meta data from temp file (i.e. before we enter the database lock) var metaDataProvider = ServiceProvider.GetService(typeof(IMetaDataProvider)) as IMetaDataProvider; if (metaDataProvider != null) { sound.MetaData = await metaDataProvider.GetMetaData(tempFile); } try { DatabaseLock.EnterWriteLock(); //move the file to the target location try { File.Move(tempFile, GetAbsoluteFileName(sound)); } catch { status = FileResultStatus.IO_ERROR; //delete the target file try { File.Delete(GetAbsoluteFileName(sound)); } catch { } throw; } return(UploadOnNewFile(sound, directory)); } finally { DatabaseLock.ExitWriteLock(); } } catch (Exception ex) { Log(ex); } finally { //delete the temp file should it still exist try { File.Delete(tempFile); } catch { } } return(new FileResult(status)); }
/// <summary> /// Creates a new directory in the given parent directory. /// </summary> /// <param name="directory"> /// Information used when adding the new sound:<list type="bullet"> /// <item><see cref="SoundboxNode.Name"/></item> /// <item><see cref="SoundboxNode.Tags"/></item> /// </list> /// </param> /// <param name="parent"> /// Null: root directory is assumed. /// </param> /// <returns></returns> public FileResult MakeDirectory(SoundboxDirectory directory, SoundboxDirectory parent) { return(GetSoundbox().MakeDirectory(directory, parent)); }
/// <summary> /// Moves a file to a new directory. There is no <see cref="Edit(SoundboxNode)"/> performed on the given file.<br/> /// If the given directory is null then the root directory is used. /// </summary> /// <param name="file"></param> /// <param name="directory"></param> /// <returns></returns> public Task <FileResult> Move(SoundboxNode file, SoundboxDirectory directory) { return(GetSoundbox().Move(file, directory)); }