2021-10-11 06:26:16 +00: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 ;
2022-10-13 08:46:01 +00:00
using osu.Framework ;
2021-10-11 06:26:16 +00:00
using osu.Framework.Extensions ;
using osu.Framework.IO.Stores ;
using osu.Framework.Logging ;
using osu.Framework.Platform ;
2021-10-13 03:51:41 +00:00
using osu.Framework.Testing ;
2021-11-19 07:07:55 +00:00
using osu.Game.Extensions ;
2022-12-13 10:53:12 +00:00
using osu.Game.IO ;
2021-10-11 06:26:16 +00:00
using osu.Game.Models ;
using Realms ;
2022-06-15 08:13:32 +00:00
namespace osu.Game.Database
2021-10-11 06:26:16 +00:00
{
/// <summary>
2021-10-12 06:46:32 +00:00
/// Handles the storing of files to the file system (and database) backing.
2021-10-11 06:26:16 +00:00
/// </summary>
2021-10-13 03:51:41 +00:00
[ExcludeFromDynamicCompile]
2021-10-11 06:26:16 +00:00
public class RealmFileStore
{
2022-01-24 10:59:58 +00:00
private readonly RealmAccess realm ;
2021-10-12 06:46:32 +00:00
2021-10-11 06:26:16 +00:00
public readonly IResourceStore < byte [ ] > Store ;
2021-10-12 06:46:32 +00:00
public readonly Storage Storage ;
2021-10-11 06:26:16 +00:00
2022-01-24 10:59:58 +00:00
public RealmFileStore ( RealmAccess realm , Storage storage )
2021-10-11 06:26:16 +00:00
{
2022-01-24 10:59:58 +00:00
this . realm = realm ;
2021-10-11 06:26:16 +00: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 08:33:44 +00: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>
2022-12-12 15:56:27 +00:00
/// <param name="preferHardLinks">Whether this import should use hard links rather than file copy operations if available.</param>
public RealmFile Add ( Stream data , Realm realm , bool addToRealm = true , bool preferHardLinks = false )
2021-10-11 06:26:16 +00:00
{
string hash = data . ComputeSHA2Hash ( ) ;
var existing = realm . Find < RealmFile > ( hash ) ;
var file = existing ? ? new RealmFile { Hash = hash } ;
if ( ! checkFileExistsAndMatchesHash ( file ) )
2022-12-12 15:56:27 +00:00
copyToStore ( file , data , preferHardLinks ) ;
2021-10-11 06:26:16 +00:00
2022-10-11 08:33:44 +00:00
if ( addToRealm & & ! file . IsManaged )
2021-10-11 06:26:16 +00:00
realm . Add ( file ) ;
return file ;
}
2022-12-12 15:56:27 +00:00
private void copyToStore ( RealmFile file , Stream data , bool preferHardLinks )
2021-10-11 06:26:16 +00:00
{
2022-12-12 15:56:27 +00:00
if ( RuntimeInfo . OS = = RuntimeInfo . Platform . Windows & & data is FileStream fs & & preferHardLinks )
2022-10-13 08:46:01 +00:00
{
// attempt to do a fast hard link rather than copy.
2022-12-15 05:17:28 +00:00
if ( HardLinkHelper . CreateHardLink ( Storage . GetFullPath ( file . GetStoragePath ( ) , true ) , fs . Name , IntPtr . Zero ) )
2022-10-13 08:46:01 +00:00
return ;
}
2021-10-11 06:26:16 +00:00
data . Seek ( 0 , SeekOrigin . Begin ) ;
2022-05-16 09:03:53 +00:00
using ( var output = Storage . CreateFileSafely ( file . GetStoragePath ( ) ) )
2021-10-11 06:26:16 +00:00
data . CopyTo ( output ) ;
data . Seek ( 0 , SeekOrigin . Begin ) ;
}
2022-12-12 15:56:27 +00:00
private bool checkFileExistsAndMatchesHash ( RealmFile file )
{
string path = file . GetStoragePath ( ) ;
// 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 ( )
{
Logger . Log ( @"Beginning realm file store cleanup" ) ;
int totalFiles = 0 ;
int removedFiles = 0 ;
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
realm . Write ( r = >
{
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
var files = r . All < RealmFile > ( ) . ToList ( ) ;
foreach ( var file in files )
{
totalFiles + + ;
if ( file . BacklinksCount > 0 )
continue ;
try
{
removedFiles + + ;
Storage . Delete ( file . GetStoragePath ( ) ) ;
r . Remove ( file ) ;
}
catch ( Exception e )
{
Logger . Error ( e , $@"Could not delete databased file {file.Hash}" ) ;
}
}
} ) ;
Logger . Log ( $@"Finished realm file store cleanup ({removedFiles} of {totalFiles} deleted)" ) ;
}
2021-10-11 06:26:16 +00:00
}
}