// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; namespace osu.Game.IO { /// /// Handles the Store and retrieval of Files/FileSets to the database backing /// public class FileStore : DatabaseBackedStore { public readonly IResourceStore Store; public new Storage Storage => base.Storage; public FileStore(IDatabaseContextFactory contextFactory, Storage storage) : base(contextFactory, storage.GetStorageForDirectory(@"files")) { Store = new StorageBackedResourceStore(Storage); } /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. public IEnumerable QueryFiles(Expression> query) => ContextFactory.Get().Set().AsNoTracking().Where(f => f.ReferenceCount > 0).Where(query); public FileInfo Add(Stream data, bool reference = true) { using (var usage = ContextFactory.GetForWrite()) { string hash = data.ComputeSHA2Hash(); var existing = usage.Context.FileInfo.FirstOrDefault(f => f.Hash == hash); var info = existing ?? new FileInfo { Hash = hash }; string path = info.StoragePath; // we may be re-adding a file to fix missing store entries. bool requiresCopy = !Storage.Exists(path); if (!requiresCopy) { // even if the file already exists, check the existing checksum for safety. using (var stream = Storage.GetStream(path)) requiresCopy |= stream.ComputeSHA2Hash() != hash; } if (requiresCopy) { data.Seek(0, SeekOrigin.Begin); using (var output = Storage.GetStream(path, FileAccess.Write)) data.CopyTo(output); data.Seek(0, SeekOrigin.Begin); } if (reference || existing == null) Reference(info); return info; } } public void Reference(params FileInfo[] files) { if (files.Length == 0) return; using (var usage = ContextFactory.GetForWrite()) { var context = usage.Context; foreach (var f in files.GroupBy(f => f.ID)) { var refetch = context.Find(f.First().ID) ?? f.First(); refetch.ReferenceCount += f.Count(); context.FileInfo.Update(refetch); } } } public void Dereference(params FileInfo[] files) { if (files.Length == 0) return; using (var usage = ContextFactory.GetForWrite()) { var context = usage.Context; foreach (var f in files.GroupBy(f => f.ID)) { var refetch = context.FileInfo.Find(f.Key); refetch.ReferenceCount -= f.Count(); context.FileInfo.Update(refetch); } } } public override void Cleanup() { using (var usage = ContextFactory.GetForWrite()) { var context = usage.Context; foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) { try { Storage.Delete(f.StoragePath); context.FileInfo.Remove(f); } catch (Exception e) { Logger.Error(e, $@"Could not delete beatmap {f}"); } } } } } }