-
-
Notifications
You must be signed in to change notification settings - Fork 181
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
342 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.