/ src / file-system-blob-store.js
file-system-blob-store.js
  1  'use strict';
  2  
  3  const fs = require('fs-plus');
  4  const path = require('path');
  5  
  6  module.exports = class FileSystemBlobStore {
  7    static load(directory) {
  8      let instance = new FileSystemBlobStore(directory);
  9      instance.load();
 10      return instance;
 11    }
 12  
 13    constructor(directory) {
 14      this.blobFilename = path.join(directory, 'BLOB');
 15      this.blobMapFilename = path.join(directory, 'MAP');
 16      this.lockFilename = path.join(directory, 'LOCK');
 17      this.reset();
 18    }
 19  
 20    reset() {
 21      this.inMemoryBlobs = new Map();
 22      this.storedBlob = Buffer.alloc(0);
 23      this.storedBlobMap = {};
 24      this.usedKeys = new Set();
 25    }
 26  
 27    load() {
 28      if (!fs.existsSync(this.blobMapFilename)) {
 29        return;
 30      }
 31      if (!fs.existsSync(this.blobFilename)) {
 32        return;
 33      }
 34  
 35      try {
 36        this.storedBlob = fs.readFileSync(this.blobFilename);
 37        this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename));
 38      } catch (e) {
 39        this.reset();
 40      }
 41    }
 42  
 43    save() {
 44      let dump = this.getDump();
 45      let blobToStore = Buffer.concat(dump[0]);
 46      let mapToStore = JSON.stringify(dump[1]);
 47  
 48      let acquiredLock = false;
 49      try {
 50        fs.writeFileSync(this.lockFilename, 'LOCK', { flag: 'wx' });
 51        acquiredLock = true;
 52  
 53        fs.writeFileSync(this.blobFilename, blobToStore);
 54        fs.writeFileSync(this.blobMapFilename, mapToStore);
 55      } catch (error) {
 56        // Swallow the exception silently only if we fail to acquire the lock.
 57        if (error.code !== 'EEXIST') {
 58          throw error;
 59        }
 60      } finally {
 61        if (acquiredLock) {
 62          fs.unlinkSync(this.lockFilename);
 63        }
 64      }
 65    }
 66  
 67    has(key) {
 68      return (
 69        this.inMemoryBlobs.has(key) || this.storedBlobMap.hasOwnProperty(key)
 70      );
 71    }
 72  
 73    get(key) {
 74      if (this.has(key)) {
 75        this.usedKeys.add(key);
 76        return this.getFromMemory(key) || this.getFromStorage(key);
 77      }
 78    }
 79  
 80    set(key, buffer) {
 81      this.usedKeys.add(key);
 82      return this.inMemoryBlobs.set(key, buffer);
 83    }
 84  
 85    delete(key) {
 86      this.inMemoryBlobs.delete(key);
 87      delete this.storedBlobMap[key];
 88    }
 89  
 90    getFromMemory(key) {
 91      return this.inMemoryBlobs.get(key);
 92    }
 93  
 94    getFromStorage(key) {
 95      if (!this.storedBlobMap[key]) {
 96        return;
 97      }
 98  
 99      return this.storedBlob.slice.apply(
100        this.storedBlob,
101        this.storedBlobMap[key]
102      );
103    }
104  
105    getDump() {
106      let buffers = [];
107      let blobMap = {};
108      let currentBufferStart = 0;
109  
110      function dump(key, getBufferByKey) {
111        let buffer = getBufferByKey(key);
112        buffers.push(buffer);
113        blobMap[key] = [currentBufferStart, currentBufferStart + buffer.length];
114        currentBufferStart += buffer.length;
115      }
116  
117      for (let key of this.inMemoryBlobs.keys()) {
118        if (this.usedKeys.has(key)) {
119          dump(key, this.getFromMemory.bind(this));
120        }
121      }
122  
123      for (let key of Object.keys(this.storedBlobMap)) {
124        if (!blobMap[key] && this.usedKeys.has(key)) {
125          dump(key, this.getFromStorage.bind(this));
126        }
127      }
128  
129      return [buffers, blobMap];
130    }
131  };