17 using System.Collections;
18 using System.Collections.Concurrent;
19 using System.Collections.Generic;
22 using System.Text.RegularExpressions;
23 using System.Threading;
40 protected const string NoReadPermissionsError =
"The current user does not have permission to read from the organization Object Store." +
41 " Please contact your organization administrator to request permission.";
46 protected const string NoWritePermissionsError =
"The current user does not have permission to write to the organization Object Store." +
47 " Please contact your organization administrator to request permission.";
52 public event EventHandler<ObjectStoreErrorRaisedEventArgs>
ErrorRaised;
62 private volatile bool _dirty;
64 private Timer _persistenceTimer;
65 private Regex _pathRegex =
new (
@"^\.?[a-zA-Z0-9\\/_#\-\$= ]+\.?[a-zA-Z0-9]*$", RegexOptions.Compiled);
66 private readonly ConcurrentDictionary<string, ObjectStoreEntry> _storage =
new();
67 private readonly
object _persistLock =
new object();
106 Log.
Trace($
"LocalObjectStore.Initialize(): Storage Root: {directoryInfo.FullName}. StorageFileCount {controls.StorageFileCount}. StorageLimit {BytesToMb(controls.StorageLimit)}MB");
120 private IEnumerable<ObjectStoreEntry> GetObjectStoreEntries(
bool loadContent,
bool takePersistLock =
true)
125 lock (takePersistLock ? _persistLock :
new object())
127 foreach (var kvp
in _storage)
129 if (!loadContent || kvp.Value.Data !=
null)
132 yield
return kvp.Value;
138 var path = NormalizePath(file.FullName.RemoveFromStart(rootFolder));
140 ObjectStoreEntry objectStoreEntry;
143 if (!_storage.TryGetValue(path, out objectStoreEntry) || objectStoreEntry.Data ==
null)
145 if(TryCreateObjectStoreEntry(file.FullName, path, out objectStoreEntry))
148 yield
return _storage[path] = objectStoreEntry;
154 if (!_storage.ContainsKey(path))
157 yield
return _storage[path] =
new ObjectStoreEntry(path,
null);
168 public ICollection<string>
Keys
172 return GetObjectStoreEntries(loadContent:
false).Select(objectStoreEntry => objectStoreEntry.Path).ToList();
196 throw new ArgumentNullException(nameof(path));
200 throw new InvalidOperationException($
"LocalObjectStore.ContainsKey(): {NoReadPermissionsError}");
203 path = NormalizePath(path);
204 if (_storage.ContainsKey(path))
213 _storage[path] =
new ObjectStoreEntry(path,
null);
229 throw new KeyNotFoundException($
"Object with path '{path}' was not found in the current project. " +
230 "Please use ObjectStore.ContainsKey(key) to check if an object exists before attempting to read."
233 path = NormalizePath(path);
235 if(!_storage.TryGetValue(path, out var objectStoreEntry) || objectStoreEntry.Data ==
null)
238 if (TryCreateObjectStoreEntry(filePath, path, out objectStoreEntry))
241 _storage[path] = objectStoreEntry;
244 return objectStoreEntry?.Data;
257 throw new ArgumentNullException(nameof(path));
261 throw new InvalidOperationException($
"LocalObjectStore.SaveBytes(): {NoWritePermissionsError}");
263 else if (!_pathRegex.IsMatch(path))
265 throw new ArgumentException($
"LocalObjectStore: path is not supported: '{path}'");
267 else if (path.Count(c => c ==
'/') > 100 || path.Count(c => c ==
'\\') > 100)
270 throw new ArgumentException($
"LocalObjectStore: path is not supported: '{path}'");
274 path = NormalizePath(path);
303 var entry = _storage[path] =
new ObjectStoreEntry(path, contents);
316 var expectedStorageSizeBytes = contents?.Length ?? 0L;
317 foreach (var kvp
in GetObjectStoreEntries(loadContent:
false, takePersistLock: takePersistLock))
319 if (path.Equals(kvp.Path))
330 expectedStorageSizeBytes += kvp.Data.Length;
342 var message = $
"LocalObjectStore.InternalSaveBytes(): You have reached the ObjectStore limit for files it can save: {fileCount}. Unable to save the new file: '{path}'";
351 var message = $
"LocalObjectStore.InternalSaveBytes(): at storage capacity: {BytesToMb(expectedStorageSizeBytes)}MB/{BytesToMb(Controls.StorageLimit)}MB. Unable to save: '{path}'";
369 throw new ArgumentNullException(nameof(path));
373 throw new InvalidOperationException($
"LocalObjectStore.Delete(): {NoWritePermissionsError}");
376 path = NormalizePath(path);
378 var wasInCache = _storage.TryRemove(path, out var _);
430 if (_persistenceTimer !=
null)
432 _persistenceTimer.Change(Timeout.Infinite, Timeout.Infinite);
436 _persistenceTimer.DisposeSafely();
439 catch (Exception err)
441 Log.
Error(err,
"Error deleting storage directory.");
450 return GetObjectStoreEntries(loadContent:
true).Select(objectStore =>
new KeyValuePair<
string,
byte[]>(objectStore.Path, objectStore.Data)).GetEnumerator();
456 IEnumerator IEnumerable.GetEnumerator()
473 private void Persist()
491 catch (Exception err)
493 Log.
Error(
"LocalObjectStore.Persist()", err);
500 if(_persistenceTimer !=
null)
506 catch (ObjectDisposedException)
525 foreach (var kvp
in _storage)
527 if(kvp.Value.Data !=
null && kvp.Value.IsDirty)
531 var parentDirectory = Path.GetDirectoryName(filePath);
539 kvp.Value.SetClean();
542 if (!_storage.Contains(kvp))
558 catch (Exception err)
560 Log.
Error(err,
"LocalObjectStore.PersistData()");
577 private static double BytesToMb(
long bytes)
579 return bytes / 1024.0 / 1024.0;
582 private static string NormalizePath(
string path)
584 if (
string.IsNullOrEmpty(path))
588 return path.TrimStart(
'.').TrimStart(
'/',
'\\').Replace(
'\\',
'/');
591 private bool TryCreateObjectStoreEntry(
string filePath,
string path, out ObjectStoreEntry objectStoreEntry)
604 objectStoreEntry =
null;
625 private class ObjectStoreEntry
627 private long _isDirty;
628 public byte[]
Data {
get; }
629 public string Path {
get; }
630 public bool IsDirty => Interlocked.Read(ref _isDirty) != 0;
631 public ObjectStoreEntry(
string path,
byte[] data)
636 public void SetDirty()
639 Interlocked.CompareExchange(ref _isDirty, 1, 0);
641 public void SetClean()
643 Interlocked.CompareExchange(ref _isDirty, 0, 1);