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();
108 Log.
Trace($
"LocalObjectStore.Initialize(): Storage Root: {directoryInfo.FullName}. StorageFileCount {controls.StorageFileCount}. StorageLimit {BytesToMb(controls.StorageLimit)}MB");
122 private IEnumerable<ObjectStoreEntry> GetObjectStoreEntries(
bool loadContent,
bool takePersistLock =
true)
127 lock (takePersistLock ? _persistLock :
new object())
129 foreach (var kvp
in _storage)
131 if (!loadContent || kvp.Value.Data !=
null)
134 yield
return kvp.Value;
140 var path = NormalizePath(file.FullName.RemoveFromStart(rootFolder));
142 ObjectStoreEntry objectStoreEntry;
145 if (!_storage.TryGetValue(path, out objectStoreEntry) || objectStoreEntry.Data ==
null)
147 if(TryCreateObjectStoreEntry(file.FullName, path, out objectStoreEntry))
150 yield
return _storage[path] = objectStoreEntry;
156 if (!_storage.ContainsKey(path))
159 yield
return _storage[path] =
new ObjectStoreEntry(path,
null);
170 public ICollection<string>
Keys
174 return GetObjectStoreEntries(loadContent:
false).Select(objectStoreEntry => objectStoreEntry.Path).ToList();
198 throw new ArgumentNullException(nameof(path));
202 throw new InvalidOperationException($
"LocalObjectStore.ContainsKey(): {NoReadPermissionsError}");
205 path = NormalizePath(path);
206 if (_storage.ContainsKey(path))
215 _storage[path] =
new ObjectStoreEntry(path,
null);
231 throw new KeyNotFoundException($
"Object with path '{path}' was not found in the current project. " +
232 "Please use ObjectStore.ContainsKey(key) to check if an object exists before attempting to read."
235 path = NormalizePath(path);
237 if(!_storage.TryGetValue(path, out var objectStoreEntry) || objectStoreEntry.Data ==
null)
240 if (TryCreateObjectStoreEntry(filePath, path, out objectStoreEntry))
243 _storage[path] = objectStoreEntry;
246 return objectStoreEntry?.Data;
259 throw new ArgumentNullException(nameof(path));
263 throw new InvalidOperationException($
"LocalObjectStore.SaveBytes(): {NoWritePermissionsError}");
265 else if (!_pathRegex.IsMatch(path))
267 throw new ArgumentException($
"LocalObjectStore: path is not supported: '{path}'");
269 else if (path.Count(c => c ==
'/') > 100 || path.Count(c => c ==
'\\') > 100)
272 throw new ArgumentException($
"LocalObjectStore: path is not supported: '{path}'");
276 path = NormalizePath(path);
305 var entry = _storage[path] =
new ObjectStoreEntry(path, contents);
318 var expectedStorageSizeBytes = contents?.Length ?? 0L;
319 foreach (var kvp
in GetObjectStoreEntries(loadContent:
false, takePersistLock: takePersistLock))
321 if (path.Equals(kvp.Path))
332 expectedStorageSizeBytes += kvp.Data.Length;
344 var message = $
"LocalObjectStore.InternalSaveBytes(): You have reached the ObjectStore limit for files it can save: {fileCount}. Unable to save the new file: '{path}'";
353 var message = $
"LocalObjectStore.InternalSaveBytes(): at storage capacity: {BytesToMb(expectedStorageSizeBytes)}MB/{BytesToMb(Controls.StorageLimit)}MB. Unable to save: '{path}'";
371 throw new ArgumentNullException(nameof(path));
375 throw new InvalidOperationException($
"LocalObjectStore.Delete(): {NoWritePermissionsError}");
378 path = NormalizePath(path);
380 var wasInCache = _storage.TryRemove(path, out var _);
424 var parent = Directory.GetParent(normalizedPathKey);
433 return normalizedPathKey;
443 if (_persistenceTimer !=
null)
445 _persistenceTimer.Change(Timeout.Infinite, Timeout.Infinite);
449 _persistenceTimer.DisposeSafely();
452 catch (Exception err)
454 Log.
Error(err,
"Error deleting storage directory.");
463 return GetObjectStoreEntries(loadContent:
true).Select(objectStore =>
new KeyValuePair<
string,
byte[]>(objectStore.Path, objectStore.Data)).GetEnumerator();
469 IEnumerator IEnumerable.GetEnumerator()
486 private void Persist()
504 catch (Exception err)
506 Log.
Error(
"LocalObjectStore.Persist()", err);
513 if(_persistenceTimer !=
null)
519 catch (ObjectDisposedException)
538 foreach (var kvp
in _storage)
540 if(kvp.Value.Data !=
null && kvp.Value.IsDirty)
544 var parentDirectory = Path.GetDirectoryName(filePath);
552 kvp.Value.SetClean();
555 if (!_storage.Contains(kvp))
571 catch (Exception err)
573 Log.
Error(err,
"LocalObjectStore.PersistData()");
590 private static double BytesToMb(
long bytes)
592 return bytes / 1024.0 / 1024.0;
595 private static string NormalizePath(
string path)
597 if (
string.IsNullOrEmpty(path))
601 return path.TrimStart(
'.').TrimStart(
'/',
'\\').Replace(
'\\',
'/');
604 private bool TryCreateObjectStoreEntry(
string filePath,
string path, out ObjectStoreEntry objectStoreEntry)
617 objectStoreEntry =
null;
638 private class ObjectStoreEntry
640 private long _isDirty;
641 public byte[]
Data {
get; }
642 public string Path {
get; }
643 public bool IsDirty => Interlocked.Read(ref _isDirty) != 0;
644 public ObjectStoreEntry(
string path,
byte[] data)
649 public void SetDirty()
652 Interlocked.CompareExchange(ref _isDirty, 1, 0);
654 public void SetClean()
656 Interlocked.CompareExchange(ref _isDirty, 0, 1);