Skip to content

Commit

Permalink
Simple File Cache
Browse files Browse the repository at this point in the history
  • Loading branch information
TimAidley committed Feb 9, 2023
1 parent fe63352 commit f5a1f68
Show file tree
Hide file tree
Showing 4 changed files with 342 additions and 0 deletions.
142 changes: 142 additions & 0 deletions Assets/Editor/Tests/TestFileCache.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/Editor/Tests/TestFileCache.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

178 changes: 178 additions & 0 deletions Assets/Scripts/Sharing/FileCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
using System;
using System.IO;
using System.Linq;
namespace TiltBrush
{
/// <summary>
/// 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.
/// </summary>
public class FileCache
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="path">The root path of the cache</param>
/// <param name="maxMegabytes">The maximum size of the cache in megabytes</param>
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();
}

/// <summary>
/// Trims folders within the cache directory if the maximum cache size is breached.
/// Works on a last-accessed basis.
/// </summary>
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;
}
}

/// <summary>
/// Determines if a given fileset exists within the cache
/// </summary>
/// <param name="fileset">The name of the fileset</param>
/// <returns>Whether it exists</returns>
public bool FilesetExists(string fileset)
{
return Directory.Exists(Path.Combine(m_Root.FullName, fileset));
}

/// <summary>
/// Checks whether a given file exists within a fileset.
/// </summary>
/// <param name="fileset"></param>
/// <param name="filename"></param>
/// <returns></returns>
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);
}

/// <summary>
/// Writes a file to a fileset
/// </summary>
/// <param name="fileset">The name of the fileset</param>
/// <param name="filename">The file within the fileset</param>
/// <param name="data">File bytes</param>
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();
}

/// <summary>
/// Read all the bytes from a file in a fileset
/// </summary>
/// <param name="fileset">The fileset</param>
/// <param name="filename">The file</param>
/// <returns>All the bytes from the file</returns>
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);
}

/// <summary>
/// Read the bytes from a file in a fileset as a stream
/// </summary>
/// <param name="fileset">fileset</param>
/// <param name="filename">filename</param>
/// <returns>A stream of the bytes in the file</returns>
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);
}

/// <summary>
/// Delete a file in a fileset
/// </summary>
/// <param name="fileset">Fileset</param>
/// <param name="filename">File</param>
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();
}

/// <summary>
/// Delete a fileset
/// </summary>
/// <param name="fileset">Fileset</param>
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();
}

/// <summary>
/// Deletes the entire cache.
/// </summary>
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);
}
}
}
11 changes: 11 additions & 0 deletions Assets/Scripts/Sharing/FileCache.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f5a1f68

Please sign in to comment.