4 using System.Collections.Generic;
8 using System.Threading.Tasks;
9 using SiliconStudio.Core.Extensions;
10 using SiliconStudio.Core.IO;
11 using SiliconStudio.Core.LZ4;
12 using SiliconStudio.Core.Serialization;
13 using SiliconStudio.Core.Serialization.Assets;
14 using SiliconStudio.Core.Serialization.Serializers;
16 namespace SiliconStudio.Core.Storage
21 [DataSerializerGlobal(null, typeof(List<string>))]
22 [DataSerializerGlobal(null, typeof(List<KeyValuePair<ObjectId, BundleOdbBackend.ObjectInfo>>))]
23 [DataSerializerGlobal(null, typeof(List<KeyValuePair<string, ObjectId>>))]
29 public const string BundleExtension =
".bundle";
34 private readonly
string vfsBundleDirectory;
36 private readonly Dictionary<ObjectId, ObjectLocation> objects =
new Dictionary<ObjectId, ObjectLocation>();
39 private readonly Dictionary<string, Stream> bundleStreams =
new Dictionary<string, Stream>();
42 private readonly Dictionary<string, string> resolvedBundles =
new Dictionary<string, string>();
44 private readonly List<LoadedBundle> loadedBundles =
new List<LoadedBundle>();
48 public delegate
Task<string> BundleResolveDelegate(
string bundleName);
53 public BundleResolveDelegate BundleResolve {
get; set; }
58 get {
return assetIndexMap; }
61 public string BundleDirectory {
get {
return vfsBundleDirectory; } }
65 BundleResolve += BundleResolve;
67 vfsBundleDirectory = vfsRootUrl +
"/bundles/";
72 BundleResolve += DefaultBundleResolve;
79 return objects.ToDictionary(pair => pair.Key, value => value.Value.Info);
83 private Task<string> DefaultBundleResolve(
string bundleName)
86 var bundleFile = VirtualFileSystem.Combine(vfsBundleDirectory, bundleName + BundleExtension);
88 return Task.FromResult(bundleFile);
90 return Task.FromResult<
string>(null);
93 private async
Task<string> ResolveBundle(
string bundleName,
bool throwExceptionIfNotFound)
97 lock (resolvedBundles)
99 if (resolvedBundles.TryGetValue(bundleName, out bundleUrl))
101 if (bundleUrl == null)
102 throw new InvalidOperationException(
string.Format(
"Bundle {0} is being loaded twice (either cyclic dependency or concurrency issue)", bundleName));
107 resolvedBundles[bundleName] = null;
110 if (BundleResolve != null)
113 foreach (BundleResolveDelegate bundleResolvedHandler
in BundleResolve.GetInvocationList())
116 bundleUrl = await bundleResolvedHandler(bundleName);
117 if (bundleUrl != null)
123 if (bundleUrl == null)
126 lock (resolvedBundles)
128 resolvedBundles.Remove(bundleName);
131 if (!throwExceptionIfNotFound)
134 throw new FileNotFoundException(
string.Format(
"Bundle {0} could not be resolved", bundleName));
138 lock (resolvedBundles)
140 resolvedBundles[bundleName] = bundleUrl;
154 if (bundleName == null)
throw new ArgumentNullException(
"bundleName");
159 foreach (var currentBundle
in loadedBundles)
161 if (currentBundle.BundleName == bundleName)
163 currentBundle.ReferenceCount++;
170 var vfsUrl = await ResolveBundle(bundleName,
true);
174 using (var packStream = VirtualFileSystem.OpenStream(vfsUrl, VirtualFileMode.Open, VirtualFileAccess.Read))
176 bundle = ReadBundleDescription(packStream);
180 foreach (var dependency
in bundle.Dependencies)
182 await LoadBundle(dependency, objectDatabaseAssetIndexMap);
187 LoadedBundle loadedBundle = null;
189 foreach (var currentBundle
in loadedBundles)
191 if (currentBundle.BundleName == bundleName)
193 loadedBundle = currentBundle;
198 if (loadedBundle == null)
200 loadedBundle =
new LoadedBundle
202 BundleName = bundleName,
204 Description = bundle,
208 loadedBundles.Add(loadedBundle);
212 loadedBundle.ReferenceCount++;
219 foreach (var objectEntry
in bundle.Objects)
221 objects[objectEntry.Key] =
new ObjectLocation { Info = objectEntry.Value, BundleUrl = vfsUrl };
226 assetIndexMap.Merge(bundle.Assets);
229 objectDatabaseAssetIndexMap.Merge(bundle.Assets);
244 UnloadBundleRecursive(bundleName, objectDatabaseAssetIndexMap);
247 foreach (var otherLoadedBundle
in loadedBundles)
249 var bundle = otherLoadedBundle.Description;
252 foreach (var objectEntry
in bundle.Objects)
254 objects[objectEntry.Key] =
new ObjectLocation { Info = objectEntry.Value, BundleUrl = otherLoadedBundle.BundleUrl };
257 assetIndexMap.Merge(bundle.Assets);
258 objectDatabaseAssetIndexMap.Merge(bundle.Assets);
265 if (bundleName == null)
throw new ArgumentNullException(
"bundleName");
269 int loadedBundleIndex = -1;
271 for (
int index = 0; index < loadedBundles.Count; index++)
273 var currentBundle = loadedBundles[index];
274 if (currentBundle.BundleName == bundleName)
276 loadedBundleIndex = index;
281 if (loadedBundleIndex == -1)
282 throw new InvalidOperationException(
"Bundle has not been loaded.");
284 var loadedBundle = loadedBundles[loadedBundleIndex];
285 var bundle = loadedBundle.Description;
286 if (--loadedBundle.ReferenceCount == 0)
292 if (bundleStreams.TryGetValue(loadedBundle.BundleUrl, out stream))
294 bundleStreams.Remove(loadedBundle.BundleUrl);
300 loadedBundles.RemoveAt(loadedBundleIndex);
303 var removedObjects =
new HashSet<ObjectId>();
304 foreach (var objectEntry
in bundle.Objects)
306 objects.Remove(objectEntry.Key);
307 removedObjects.Add(objectEntry.Key);
311 assetIndexMap.Unmerge(bundle.Assets);
314 objectDatabaseAssetIndexMap.Unmerge(bundle.Assets);
317 foreach (var dependency
in bundle.Dependencies)
319 UnloadBundleRecursive(dependency, objectDatabaseAssetIndexMap);
340 var header = binaryReader.Read<
Header>();
343 result.Header = header;
348 throw new InvalidOperationException(
"Invalid bundle header");
352 if (header.Size != stream.Length)
354 throw new InvalidOperationException(
"Bundle has not been properly written");
358 List<string> dependencies = result.Dependencies;
359 binaryReader.Serialize(ref dependencies, ArchiveMode.Deserialize);
362 List<KeyValuePair<ObjectId, ObjectInfo>> objects = result.Objects;
363 binaryReader.Serialize(ref objects, ArchiveMode.Deserialize);
366 List<KeyValuePair<string, ObjectId>> assets = result.Assets;
367 binaryReader.Serialize(ref assets, ArchiveMode.Deserialize);
372 public static void CreateBundle(
string vfsUrl,
IOdbBackend backend,
ObjectId[] objectIds, ISet<ObjectId> disableCompressionIds, Dictionary<string, ObjectId> indexMap, IList<string> dependencies)
374 if (objectIds.Length == 0)
375 throw new InvalidOperationException(
"Nothing to pack.");
382 using (var packStream = VirtualFileSystem.OpenStream(vfsUrl, VirtualFileMode.Open, VirtualFileAccess.Read))
384 var bundle = ReadBundleDescription(packStream);
388 && ArrayExtensions.ArraysEqual(bundle.Assets.OrderBy(x => x.Key).ToList(), indexMap.OrderBy(x => x.Key).ToList())
389 &&
ArrayExtensions.ArraysEqual(bundle.Objects.Select(x => x.Key).OrderBy(x => x).ToList(), objectIds.OrderBy(x => x).ToList()))
402 using (var packStream = VirtualFileSystem.OpenStream(vfsUrl, VirtualFileMode.Create, VirtualFileAccess.Write))
404 var header =
new Header();
405 header.MagicHeader = Header.MagicHeaderValid;
408 binaryWriter.Write(header);
411 binaryWriter.Write(dependencies.ToList());
414 var objectIdPosition = packStream.Position;
417 var objects =
new List<KeyValuePair<ObjectId, ObjectInfo>>();
418 for (
int i = 0; i < objectIds.Length; ++i)
420 objects.Add(
new KeyValuePair<ObjectId, ObjectInfo>(objectIds[i],
new ObjectInfo()));
423 binaryWriter.Write(objects);
427 binaryWriter.Write(indexMap.ToList());
429 for (
int i = 0; i < objectIds.Length; ++i)
431 using (var objectStream = backend.OpenStream(objectIds[i]))
434 var objectInfo =
new ObjectInfo { StartOffset = packStream.Position, SizeNotCompressed = objectStream.Length };
437 var inputStream = objectStream;
438 var originalStreamLength = objectStream.Length;
440 var chunkHeader = ChunkHeader.Read(streamReader);
441 if (chunkHeader != null)
444 var reorderedStream =
new MemoryStream((
int)originalStreamLength);
448 chunkHeader.Write(streamWriter);
451 var newOffsetReferences = reorderedStream.Position;
452 inputStream.Position = chunkHeader.OffsetToReferences;
453 inputStream.CopyTo(reorderedStream);
456 var newOffsetObject = reorderedStream.Position;
457 inputStream.Position = chunkHeader.OffsetToObject;
458 inputStream.CopyTo(reorderedStream, chunkHeader.OffsetToReferences - chunkHeader.OffsetToObject);
461 chunkHeader.OffsetToObject = (int)newOffsetObject;
462 chunkHeader.OffsetToReferences = (int)newOffsetReferences;
463 reorderedStream.Position = 0;
464 chunkHeader.Write(streamWriter);
467 inputStream = reorderedStream;
468 inputStream.Position = 0;
472 if (!disableCompressionIds.Contains(objectIds[i]))
474 objectInfo.IsCompressed =
true;
476 var lz4OutputStream =
new LZ4Stream(packStream, CompressionMode.Compress);
477 inputStream.CopyTo(lz4OutputStream);
478 lz4OutputStream.Flush();
483 inputStream.CopyTo(packStream);
487 if (chunkHeader != null)
488 inputStream.Dispose();
491 objectInfo.EndOffset = packStream.Position;
492 objects.Add(
new KeyValuePair<ObjectId, ObjectInfo>(objectIds[i], objectInfo));
497 header.Size = packStream.Length;
498 packStream.Position = 0;
499 binaryWriter.Write(header);
502 packStream.Position = objectIdPosition;
503 binaryWriter.Write(objects);
509 ObjectLocation objectLocation;
512 if (!objects.TryGetValue(objectId, out objectLocation))
513 throw new FileNotFoundException();
522 if (bundleStreams.TryGetValue(objectLocation.BundleUrl, out stream))
525 bundleStreams.Remove(objectLocation.BundleUrl);
529 stream = VirtualFileSystem.OpenStream(objectLocation.BundleUrl, VirtualFileMode.Open, VirtualFileAccess.Read);
533 if (objectLocation.Info.IsCompressed)
535 stream.Position = objectLocation.Info.StartOffset;
536 return new PackageFileStreamLZ4(
this, objectLocation.BundleUrl, stream, CompressionMode.Decompress, objectLocation.Info.SizeNotCompressed, objectLocation.Info.EndOffset - objectLocation.Info.StartOffset);
539 return new PackageFileStream(
this, objectLocation.BundleUrl, stream, objectLocation.Info.StartOffset, objectLocation.Info.EndOffset,
false);
546 var objectInfo = objects[objectId].Info;
547 return (
int)(objectInfo.EndOffset - objectInfo.StartOffset);
553 throw new NotSupportedException();
558 throw new NotSupportedException();
565 return objects.ContainsKey(objectId);
573 return objects.Select(x => x.Key).ToList();
579 throw new NotSupportedException();
584 throw new NotSupportedException();
587 private struct ObjectLocation
589 public ObjectInfo Info;
590 public string BundleUrl;
593 private class LoadedBundle
595 public string BundleName;
596 public string BundleUrl;
597 public int ReferenceCount;
598 public BundleDescription Description;
601 internal void ReleasePackageStream(
string packageLocation,
Stream stream)
605 if (!bundleStreams.ContainsKey(packageLocation))
607 bundleStreams.Add(packageLocation, stream);
629 stream.Serialize(ref obj.StartOffset);
630 stream.Serialize(ref obj.EndOffset);
631 stream.Serialize(ref obj.SizeNotCompressed);
641 public const uint MagicHeaderValid = 0x42584450;
651 stream.Serialize(ref obj.MagicHeader);
652 stream.Serialize(ref obj.Size);
653 stream.Serialize(ref obj.Crc);
660 private readonly
string packageLocation;
661 private readonly
Stream innerStream;
664 : base(innerStream, compressionMode, uncompressedSize: uncompressedStreamSize, compressedSize: compressedSize, disposeInnerStream:
false)
666 this.bundleOdbBackend = bundleOdbBackend;
667 this.packageLocation = packageLocation;
668 this.innerStream = innerStream;
671 protected override void Dispose(
bool disposing)
673 bundleOdbBackend.ReleasePackageStream(packageLocation, innerStream);
675 base.Dispose(disposing);
682 private readonly
string packageLocation;
684 public PackageFileStream(
BundleOdbBackend bundleOdbBackend,
string packageLocation,
Stream internalStream,
long startPosition = 0,
long endPosition = -1,
bool disposeInternalStream =
true,
bool seekToBeginning =
true)
685 : base(internalStream, startPosition, endPosition, disposeInternalStream, seekToBeginning)
687 this.bundleOdbBackend = bundleOdbBackend;
688 this.packageLocation = packageLocation;
691 protected override void Dispose(
bool disposing)
693 bundleOdbBackend.ReleasePackageStream(packageLocation, virtualFileStream ?? InternalStream);
696 virtualFileStream = null;
698 base.Dispose(disposing);
704 var bundleFiles = VirtualFileSystem.ListFiles(vfsBundleDirectory,
"*.bundle", VirtualSearchOption.TopDirectoryOnly).Result;
708 bundleFiles = bundleFiles.Union(VirtualFileSystem.ListFiles(vfsBundleDirectory,
"*.mp3", VirtualSearchOption.TopDirectoryOnly).Result).ToArray();
710 foreach (var bundleFile
in bundleFiles)
712 var bundleRealFile = VirtualFileSystem.GetAbsolutePath(bundleFile);
715 if (bundleRealFile.EndsWith(
".mp3", StringComparison.CurrentCultureIgnoreCase))
716 bundleRealFile = bundleRealFile.Substring(0, bundleRealFile.Length - 4);
718 if (bundleFileDeletePredicate(bundleRealFile))
720 NativeFile.FileDelete(bundleRealFile);
Virtual abstraction over a file system. It handles access to files, http, packages, path rewrite, etc...
async Task LoadBundle(string bundleName, ObjectDatabaseAssetIndexMap objectDatabaseAssetIndexMap)
Loads the specified bundle.
int GetSize(ObjectId objectId)
Requests that this backend read an object's length (but not its contents).
ObjectId Write(ObjectId objectId, Stream dataStream, int length, bool forceWrite)
Writes an object to the backing store. The backend may need to compute the object ID and return it to...
Asset Index Map implementation which regroups all the asset index maps of every loaded file backend a...
static bool FileExists(string path)
Checks the existence of a file.
VirtualFileShare
File share capabilities, equivalent of System.IO.FileShare.
Implements SerializationStream as a binary writer.
Stream OpenStream(ObjectId objectId, VirtualFileMode mode=VirtualFileMode.Open, VirtualFileAccess access=VirtualFileAccess.Read, VirtualFileShare share=VirtualFileShare.Read)
Opens a NativeStream of the object with the specified ObjectId.
Base class for custom object database backends (ODB).
OdbStreamWriter CreateStream()
Creates a stream that will be saved to database when closed and/or disposed.
static bool DirectoryExists(string path)
Checks the existence of a directory.
override void Dispose(bool disposing)
Dictionary< ObjectId, ObjectInfo > GetObjectInfos()
string GetFilePath(ObjectId objectId)
Returns the file path corresponding to the given id (in the VFS domain), if appliable.
Base class for implementation of SerializationStream.
override void Dispose(bool disposing)
Implements SerializationStream as a binary reader.
Object Database Backend (ODB) implementation that bundles multiple chunks into a .bundle files, optionally compressed with LZ4.
BundleOdbBackend(string vfsRootUrl)
void UnloadBundle(string bundleName, ObjectDatabaseAssetIndexMap objectDatabaseAssetIndexMap)
Unload the specified bundle.
bool Exists(ObjectId objectId)
Determines weither the object with the specified ObjectId exists.
VirtualFileAccess
File access equivalent of System.IO.FileAccess.
static void CreateBundle(string vfsUrl, IOdbBackend backend, ObjectId[] objectIds, ISet< ObjectId > disableCompressionIds, Dictionary< string, ObjectId > indexMap, IList< string > dependencies)
VirtualFileMode
File mode equivalent of System.IO.FileMode.
Block compression stream. Allows to use LZ4 for stream compression.
PackageFileStreamLZ4(BundleOdbBackend bundleOdbBackend, string packageLocation, Stream innerStream, CompressionMode compressionMode, long uncompressedStreamSize, long compressedSize)
IEnumerable< ObjectId > EnumerateObjects()
Enumerates the object stored in this backend.
Description of a bundle: header, dependencies, objects and assets.
static void CreateDirectory(string path)
Creates all directories so that path exists.
_Use_decl_annotations_ bool IsCompressed(DXGI_FORMAT fmt)
Compression
Compression method enumeration
A hash to uniquely identify data.
Describes how to serialize and deserialize an object without knowing its type. Used as a common base ...
ArchiveMode
Enumerates the different mode of serialization (either serialization or deserialization).
void Delete(ObjectId objectId)
Deletes the specified ObjectId.
A multithreaded wrapper over a Stream, used by the VirtualFileSystem. It also allows restricted acces...
PackageFileStream(BundleOdbBackend bundleOdbBackend, string packageLocation, Stream internalStream, long startPosition=0, long endPosition=-1, bool disposeInternalStream=true, bool seekToBeginning=true)
void DeleteBundles(Func< string, bool > bundleFileDeletePredicate)
static BundleDescription ReadBundleDescription(Stream stream)
Reads the bundle description.