whitesharx/httx

View on GitHub
Assets/Httx/Runtime/Caches/Disk/Editor.cs

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright (c) 2020 Sergey Ivonchik
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
// OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.IO;

namespace Httx.Caches.Disk {
  /// <summary>
  /// Edits the values for an entry.
  /// </summary>
  public class Editor {
    private readonly bool[] written;
    private readonly DiskLruCache parent;

    public Editor(UnsafeEntry entry, DiskLruCache parent) {
      this.parent = parent;

      Entry = entry;
      written = entry.UnsafeReadable ? null : new bool[parent.ValueCount];
    }

    /// <summary>
    /// Returns an unbuffered input stream to read the last committed value,
    /// or null if no value has been committed.
    /// </summary>
    public Stream ReaderInstanceAt(int index) {
      lock (parent) {
        if (this != Entry.UnsafeCurrentEditor) {
          throw new InvalidOperationException();
        }

        if (!Entry.UnsafeReadable) {
          return null;
        }

        try {
          return Entry.CleanFileAt(index).OpenRead();
        } catch (FileNotFoundException) {
          return null;
        }
      }
    }

    /// <summary>
    /// Returns the last committed value as a string, or null if no value
    /// has been committed.
    /// </summary>
    public string StringAt(int index) {
      var inputStream = ReaderInstanceAt(index);
      return null != inputStream ? new StreamReader(inputStream).ReadToEnd() : null;
    }

    /// <summary>
    /// Returns a new unbuffered output stream to write the value at
    /// index. If the underlying output stream encounters errors
    /// when writing to the filesystem, this edit will be aborted when
    /// commit is called. The returned output stream does not throw
    /// IOExceptions.
    /// </summary>
    public Stream WriterInstanceAt(int index) {
      if (index < 0 || index >= parent.ValueCount) {
        throw new ArgumentException($"Expected index {index} to "
            + "be greater than 0 and less than the maximum value count "
            + $"of {parent.ValueCount}");
      }

      lock (parent) {
        if (this != Entry.UnsafeCurrentEditor) {
          throw new InvalidOperationException();
        }

        if (!Entry.UnsafeReadable) {
          written[index] = true;
        }

        var dirtyFile = Entry.DirtyFileAt(index);
        Stream outputStream;

        try {
          outputStream = dirtyFile.Open(FileMode.Append, FileAccess.Write);
        } catch (DirectoryNotFoundException) {
          Directory.CreateDirectory(parent.Directory.FullName);

          try {
            outputStream = dirtyFile.Open(FileMode.Append, FileAccess.Write);
          } catch (DirectoryNotFoundException) {
            // We are unable to recover. Silently eat the writes.
            // return NULL_OUTPUT_STREAM;
            outputStream = null;
          }
        }

        return outputStream;
      }
    }

    /// <summary>
    /// Sets the value at index to value.
    /// </summary>
    public void PutAt(int index, string value) {
      using (var writer = new StreamWriter(WriterInstanceAt(index))) {
        writer.Write(value);
      }
    }

    public void PutAt(int index, byte[] value) {
      using (var writer = new BinaryWriter(WriterInstanceAt(index))) {
        writer.Write(value);
      }
    }

    /// <summary>
    /// Sets the value at 0 index to value.
    /// </summary>
    public void Put(string value) => PutAt(0, value);

    public void Put(byte[] value) => PutAt(0, value);

    /// <summary>
    /// Commits this edit so it is visible to readers.  This releases the
    /// edit lock so another edit may be started on the same key.
    /// </summary>
    public void Commit() {
      try {
        parent.UnsafeCompleteEdit(this, true);
      } catch (Exception) {
        parent.UnsafeCompleteEdit(this, false);
        parent.Remove(Entry.Key); // The previous entry is stale.
      }

      Committed = true;
    }

    /// <summary>
    /// Aborts this edit. This releases the edit lock so another edit may be
    /// started on the same key.
    /// </summary>
    public void Abort() {
      parent.UnsafeCompleteEdit(this, false);
    }

    public bool Committed { get; private set; }

    public UnsafeEntry Entry { get; }

    public IEnumerable<bool> Written => written;

    public bool WrittenAt(int index) => written[index];
  }
}