diff --git a/Assets/Editor/Tests/TestFileCache.cs b/Assets/Editor/Tests/TestFileCache.cs new file mode 100644 index 0000000000..ba10bd1e58 --- /dev/null +++ b/Assets/Editor/Tests/TestFileCache.cs @@ -0,0 +1,142 @@ +using System.IO; +using System.Linq; +using NUnit.Framework; + +namespace TiltBrush +{ + [TestFixture] + internal class TestFileCache + { + private FileCache m_Cache; + private string m_Path; + [SetUp] + public void Setup() + { + m_Path = Path.Combine(Path.GetTempPath(), "FileCacheTest"); + if (File.Exists(m_Path)) + { + File.Delete(m_Path); + } + if (Directory.Exists(m_Path)) + { + Directory.Delete(m_Path, recursive: true); + } + m_Cache = new FileCache(m_Path, 1); + } + + [TearDown] + public void Teardown() + { + if (Directory.Exists(m_Path)) + { + Directory.Delete(m_Path, recursive: true); + } + } + + [Test] + public void IsDirectoryCreated() + { + Assert.IsTrue(Directory.Exists(m_Path)); + } + + [Test] + public void IsCacheSizeUpdated() + { + Assert.That(m_Cache.CacheSize == 0); + byte[] bytes = new byte[1000]; + m_Cache.Write("test", "onethousand", bytes); + Assert.That(m_Cache.CacheSize == 1000); + } + + [Test] + public void IsCacheLimitRespected() + { + byte[] bytes = new byte[100000]; + for (int i = 0; i < 11; i++) + { + m_Cache.Write($"test_{i}", "100kbytes", bytes); + } + Assert.That(m_Cache.CacheSize == 1000000); + var rootDir = new DirectoryInfo(m_Path); + Assert.That(rootDir.EnumerateFiles("*", SearchOption.AllDirectories) + .Sum(x => x.Length) == 1000000); + } + + [Test] + public void IsLastCreatedExpunged() + { + byte[] bytes = new byte[100000]; + for (int i = 0; i < 11; i++) + { + m_Cache.Write($"test_{i}", "100kbytes", bytes); + } + Assert.IsFalse(m_Cache.FilesetExists("test_0")); + } + + [Test] + public void CanWriteMultipleFilesSetAndFiles() + { + byte[] bytes = new byte[1000]; + for (int i = 0; i < 5; i++) + { + for (int j = 0; j < 5; ++j) + { + m_Cache.Write($"test_{i}", $"100kbytes_{j}", bytes); + } + } + + for (int i = 0; i < 5; i++) + { + Assert.That(m_Cache.FilesetExists($"test_{i}")); + for (int j = 0; j < 5; ++j) + { + Assert.That(m_Cache.FileExists($"test_{i}", $"100kbytes_{j}")); + } + } + } + + [Test] + public void ThingsThatDontExistDontExist() + { + byte[] bytes = new byte[1000]; + m_Cache.Write("Real", "onethousand", bytes); + + Assert.IsFalse(m_Cache.FilesetExists("Imaginary")); + Assert.IsFalse(m_Cache.FileExists("Imaginary", "onethousand")); + Assert.IsFalse(m_Cache.FileExists("Real", "twothousand")); + } + + [Test] + public void FilesCanBeDeleted() + { + byte[] bytes = new byte[1000]; + m_Cache.Write("Real", "onethousand", bytes); + Assert.That(m_Cache.FileExists("Real", "onethousand")); + m_Cache.DeleteFile("Real", "onethousand"); + Assert.That(!m_Cache.FileExists("Real", "onethousand")); + } + + [Test] + public void FileHasTheRightContents() + { + byte[] bytes = Enumerable.Range(0, 127).Select(x => (byte)x).ToArray(); + m_Cache.Write("test", "data", bytes); + byte[] read = m_Cache.Read("test", "data"); + Assert.That(bytes.SequenceEqual(read)); + } + + [Test] + public void TestStreamRead() + { + byte[] bytes = Enumerable.Range(0, 127).Select(x => (byte)x).ToArray(); + m_Cache.Write("test", "data", bytes); + using (var stream = m_Cache.ReadStream("test", "data")) + { + for (int i = 0; i < 127; ++i) + { + Assert.That(stream.ReadByte() == i); + } + } + } + } +} diff --git a/Assets/Editor/Tests/TestFileCache.cs.meta b/Assets/Editor/Tests/TestFileCache.cs.meta new file mode 100644 index 0000000000..77e6065cda --- /dev/null +++ b/Assets/Editor/Tests/TestFileCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 064578b627c24b20bf936bf824721a81 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Sharing/FileCache.cs b/Assets/Scripts/Sharing/FileCache.cs new file mode 100644 index 0000000000..47e6cf1cbf --- /dev/null +++ b/Assets/Scripts/Sharing/FileCache.cs @@ -0,0 +1,178 @@ +using System; +using System.IO; +using System.Linq; +namespace TiltBrush +{ + /// + /// The FileCache is a simple cache that can store files in multiple 'filesets'. + /// The idea is that a fileset is effectively a uniquely named folder that can store files. + /// + public class FileCache + { + /// + /// Constructor + /// + /// The root path of the cache + /// The maximum size of the cache in megabytes + public FileCache(string path, long maxMegabytes) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + m_Root = new DirectoryInfo(path); + if (!m_Root.Exists) + { + m_Root.Create(); + } + + m_MaxBytes = maxMegabytes * 1024 * 1024; + ReadCacheSize(); + TrimCacheSize(); + } + + /// + /// Trims folders within the cache directory if the maximum cache size is breached. + /// Works on a last-accessed basis. + /// + public void TrimCacheSize() + { + foreach (var subdir in m_Root.EnumerateDirectories().OrderBy(x => x.LastWriteTimeUtc)) + { + if (m_CurrentBytes < m_MaxBytes) + { + m_Root.Refresh(); + return; + } + long subdirSize = subdir.EnumerateFiles("*", SearchOption.AllDirectories).Sum(x => x.Length); + subdir.Delete(recursive: true); + m_CurrentBytes -= subdirSize; + } + } + + /// + /// Determines if a given fileset exists within the cache + /// + /// The name of the fileset + /// Whether it exists + public bool FilesetExists(string fileset) + { + return Directory.Exists(Path.Combine(m_Root.FullName, fileset)); + } + + /// + /// Checks whether a given file exists within a fileset. + /// + /// + /// + /// + public bool FileExists(string fileset, string filename) + { + string folder = Path.Combine(m_Root.FullName, fileset); + string path = Path.Combine(folder, filename); + return File.Exists(path); + } + + /// + /// Writes a file to a fileset + /// + /// The name of the fileset + /// The file within the fileset + /// File bytes + public void Write(string fileset, string filename, byte[] data) + { + string folder = Path.Combine(m_Root.FullName, fileset); + string path = Path.Combine(folder, filename); + DirectoryInfo subdir = new DirectoryInfo(folder); + bool createDir = !subdir.Exists; + if (createDir) + { + Directory.CreateDirectory(folder); + } + else + { + subdir.LastWriteTimeUtc = DateTime.UtcNow; + } + File.WriteAllBytes(path, data); + m_CurrentBytes += data.LongLength; + if (createDir) + { + m_Root.Refresh(); + } + TrimCacheSize(); + } + + /// + /// Read all the bytes from a file in a fileset + /// + /// The fileset + /// The file + /// All the bytes from the file + public byte[] Read(string fileset, string filename) + { + string folder = Path.Combine(m_Root.FullName, fileset); + string path = Path.Combine(folder, filename); + return File.ReadAllBytes(path); + } + + /// + /// Read the bytes from a file in a fileset as a stream + /// + /// fileset + /// filename + /// A stream of the bytes in the file + public Stream ReadStream(string fileset, string filename) + { + string folder = Path.Combine(m_Root.FullName, fileset); + string path = Path.Combine(folder, filename); + return File.OpenRead(path); + } + + /// + /// Delete a file in a fileset + /// + /// Fileset + /// File + public void DeleteFile(string fileset, string filename) + { + string folder = Path.Combine(m_Root.FullName, fileset); + string path = Path.Combine(folder, filename); + FileInfo file = new FileInfo(path); + m_CurrentBytes -= file.Length; + file.Delete(); + } + + /// + /// Delete a fileset + /// + /// Fileset + public void DeleteFileset(string fileset) + { + string folder = Path.Combine(m_Root.FullName, fileset); + DirectoryInfo subdir = new DirectoryInfo(folder); + m_CurrentBytes -= subdir.EnumerateFiles("*", SearchOption.AllDirectories).Sum(x => x.Length); + Directory.Delete(fileset, recursive: true); + m_Root.Refresh(); + } + + /// + /// Deletes the entire cache. + /// + public void Clear() + { + m_Root.Delete(recursive: true); + } + + public long CacheSize => m_CurrentBytes; + + private DirectoryInfo m_Root; + private long m_MaxBytes; + private long m_CurrentBytes; + + private void ReadCacheSize() + { + m_CurrentBytes = m_Root.EnumerateFiles("*", SearchOption.AllDirectories).Sum(x => x.Length); + } + } +} diff --git a/Assets/Scripts/Sharing/FileCache.cs.meta b/Assets/Scripts/Sharing/FileCache.cs.meta new file mode 100644 index 0000000000..d77ca352f7 --- /dev/null +++ b/Assets/Scripts/Sharing/FileCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ddd12e6ee0b04c72ad52fe1829c5d225 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: