2021-10-11 14:26:16 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System ;
using System.IO ;
using System.Linq ;
using osu.Framework.Extensions ;
using osu.Framework.IO.Stores ;
using osu.Framework.Logging ;
using osu.Framework.Platform ;
2021-10-13 11:51:41 +08:00
using osu.Framework.Testing ;
2021-11-19 15:07:55 +08:00
using osu.Game.Extensions ;
2021-10-11 14:26:16 +08:00
using osu.Game.Models ;
using Realms ;
2022-06-15 16:13:32 +08:00
namespace osu.Game.Database
2021-10-11 14:26:16 +08:00
{
/// <summary>
2021-10-12 14:46:32 +08:00
/// Handles the storing of files to the file system (and database) backing.
2021-10-11 14:26:16 +08:00
/// </summary>
2021-10-13 11:51:41 +08:00
[ExcludeFromDynamicCompile]
2021-10-11 14:26:16 +08:00
public class RealmFileStore
{
2022-01-24 18:59:58 +08:00
private readonly RealmAccess realm ;
2021-10-12 14:46:32 +08:00
2021-10-11 14:26:16 +08:00
public readonly IResourceStore < byte [ ] > Store ;
2021-10-12 14:46:32 +08:00
public readonly Storage Storage ;
2021-10-11 14:26:16 +08:00
2022-01-24 18:59:58 +08:00
public RealmFileStore ( RealmAccess realm , Storage storage )
2021-10-11 14:26:16 +08:00
{
2022-01-24 18:59:58 +08:00
this . realm = realm ;
2021-10-11 14:26:16 +08:00
Storage = storage . GetStorageForDirectory ( @"files" ) ;
Store = new StorageBackedResourceStore ( Storage ) ;
}
/// <summary>
/// Add a new file to the game-wide database, copying it to permanent storage if not already present.
/// </summary>
/// <param name="data">The file data stream.</param>
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
2022-10-11 16:33:44 +08:00
/// <param name="addToRealm">Whether the <see cref="RealmFile"/> should immediately be added to the underlying realm. If <c>false</c> is provided here, the instance must be manually added.</param>
public RealmFile Add ( Stream data , Realm realm , bool addToRealm = true )
2021-10-11 14:26:16 +08:00
{
string hash = data . ComputeSHA2Hash ( ) ;
var existing = realm . Find < RealmFile > ( hash ) ;
var file = existing ? ? new RealmFile { Hash = hash } ;
if ( ! checkFileExistsAndMatchesHash ( file ) )
copyToStore ( file , data ) ;
2022-10-11 16:33:44 +08:00
if ( addToRealm & & ! file . IsManaged )
2021-10-11 14:26:16 +08:00
realm . Add ( file ) ;
return file ;
}
private void copyToStore ( RealmFile file , Stream data )
{
data . Seek ( 0 , SeekOrigin . Begin ) ;
2022-05-16 17:03:53 +08:00
using ( var output = Storage . CreateFileSafely ( file . GetStoragePath ( ) ) )
2021-10-11 14:26:16 +08:00
data . CopyTo ( output ) ;
data . Seek ( 0 , SeekOrigin . Begin ) ;
}
private bool checkFileExistsAndMatchesHash ( RealmFile file )
{
2021-11-19 15:07:55 +08:00
string path = file . GetStoragePath ( ) ;
2021-10-11 14:26:16 +08:00
// we may be re-adding a file to fix missing store entries.
if ( ! Storage . Exists ( path ) )
return false ;
// even if the file already exists, check the existing checksum for safety.
using ( var stream = Storage . GetStream ( path ) )
return stream . ComputeSHA2Hash ( ) = = file . Hash ;
}
public void Cleanup ( )
{
2021-11-25 13:17:42 +08:00
Logger . Log ( @"Beginning realm file store cleanup" ) ;
int totalFiles = 0 ;
int removedFiles = 0 ;
2021-10-11 14:26:16 +08:00
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
2022-01-25 12:09:47 +08:00
realm . Write ( r = >
2021-10-11 14:26:16 +08:00
{
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
2022-01-25 12:09:47 +08:00
var files = r . All < RealmFile > ( ) . ToList ( ) ;
2021-10-11 14:26:16 +08:00
foreach ( var file in files )
{
2021-11-25 13:17:42 +08:00
totalFiles + + ;
2021-10-11 14:26:16 +08:00
if ( file . BacklinksCount > 0 )
continue ;
try
{
2021-11-25 13:17:42 +08:00
removedFiles + + ;
2021-11-19 15:07:55 +08:00
Storage . Delete ( file . GetStoragePath ( ) ) ;
2022-01-25 12:09:47 +08:00
r . Remove ( file ) ;
2021-10-11 14:26:16 +08:00
}
catch ( Exception e )
{
Logger . Error ( e , $@"Could not delete databased file {file.Hash}" ) ;
}
}
2022-01-21 16:08:20 +08:00
} ) ;
2021-11-25 13:17:42 +08:00
Logger . Log ( $@"Finished realm file store cleanup ({removedFiles} of {totalFiles} deleted)" ) ;
2021-10-11 14:26:16 +08:00
}
}
}