perryrh0dan/taskline

View on GitHub
src/local.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { join, basename } from 'path';
import { randomBytes } from 'crypto';
import * as os from 'os';
import * as fs from 'fs';

import { Storage } from './storage';
import { Config } from './config';
import { Item } from './item';
import { Task } from './task';
import { Note } from './note';
import { Renderer } from './renderer';

// const render = require('./render')

export class LocalStorage implements Storage {
  private static _instance: LocalStorage;
  private storageDir: string = '';
  private archiveDir: string = '';
  private tempDir: string = '';
  private archiveFile: string = '';
  private mainStorageFile: string = '';

  public static get instance(): LocalStorage {
    if (!this._instance) {
      this._instance = new LocalStorage();
      this._instance.init();
    }

    return this._instance;
  }

  private init(): void{
    this.storageDir = join(this.mainAppDir, 'storage');
    this.archiveDir = join(this.mainAppDir, 'archive');
    this.tempDir = join(this.mainAppDir, '.temp');
    this.archiveFile = join(this.archiveDir, 'archive.json');
    this.mainStorageFile = join(this.storageDir, 'storage.json');

    this.ensureDirectories();
  }

  private get mainAppDir(): string {
    const {
      tasklineDirectory
    } = Config.instance.get();
    const defaultAppDirectory: string = join(os.homedir(), '.taskline');

    if (!tasklineDirectory) {
      return defaultAppDirectory;
    }

    if (!fs.existsSync(tasklineDirectory)) {
      Renderer.instance.invalidCustomAppDir(tasklineDirectory);
      process.exit(1);
    }

    return join(tasklineDirectory, '.taskline');
  }

  private ensureMainAppDir(): void {
    if (!fs.existsSync(this.mainAppDir)) {
      fs.mkdirSync(this.mainAppDir);
    }
  }

  private ensureStorageDir(): void {
    if (!fs.existsSync(this.storageDir)) {
      fs.mkdirSync(this.storageDir);
    }
  }

  private ensureTempDir(): void {
    if (!fs.existsSync(this.tempDir)) {
      fs.mkdirSync(this.tempDir);
    }
  }

  private ensureArchiveDir(): void {
    if (!fs.existsSync(this.archiveDir)) {
      fs.mkdirSync(this.archiveDir);
    }
  }

  private cleanTempDir(): void {
    const tempFiles: Array<string> = fs
      .readdirSync(this.tempDir)
      .map((x: string) => join(this.tempDir, x));

    if (tempFiles.length !== 0) {
      tempFiles.forEach((tempFile: string) => fs.unlinkSync(tempFile));
    }
  }

  private ensureDirectories(): void {
    this.ensureMainAppDir();
    this.ensureStorageDir();
    this.ensureArchiveDir();
    this.ensureTempDir();
    this.cleanTempDir();
  }

  private getRandomHexString(length: number = 8): string {
    return randomBytes(Math.ceil(length / 2))
      .toString('hex')
      .slice(0, length);
  }

  private getTempFile(filePath: string): string {
    const randomString: string = this.getRandomHexString();
    const tempFilename: string = basename(filePath)
      .split('.')
      .join(`.TEMP-${randomString}.`);
    return join(this.tempDir, tempFilename);
  }

  private parseJson(data: any): Array<Item> {
    const items: Array<Item> = new Array<Item>();

    Object.keys(data).forEach((id: string) => {
      if (data[id].isTask) {
        items.push(new Task(data[id]));
      } else if (data[id].isTask === false){
        items.push(new Note(data[id]));
      }

      // to support old storage format
      if (data[id]._isTask) {
        items.push(new Task({
          id: data[id]._id,
          date: data[id]._date,
          timestamp: data[id]._timestamp,
          description: data[id].description,
          isStarred: data[id].isStarred,
          boards: data[id].boards,
          priority: data[id].priority,
          inProgress: data[id].inProgress,
          isCanceled: data[id].isCanceled,
          isComplete: data[id].isComplete,
          dueDate: data[id].dueDate
        }));
      } else if (data[id]._isTask === false) {
        items.push(new Note({
          id: data[id]._id,
          date: data[id]._date,
          timestamp: data[id]._timestamp,
          description: data[id].description,
          isStarred: data[id].isStarred,
          boards: data[id].boards
        }));
      }
    });

    return items;
  }

  public async get(ids?: Array<number>): Promise<Array<Item>> {
    let data: Array<Item> = new Array<Item>();

    if (fs.existsSync(this.mainStorageFile)) {
      const content: string = fs.readFileSync(this.mainStorageFile, 'utf8');
      const jsonData: string = JSON.parse(content);
      data = this.parseJson(jsonData);
    }

    if (ids) {
      return this.filterByID(data, ids);
    }

    return data;
  }

  public async getArchive(ids?: Array<number>): Promise<Array<Item>> {
    let archive: Array<Item> = new Array<Item>();

    if (fs.existsSync(this.archiveFile)) {
      const content: string = fs.readFileSync(this.archiveFile, 'utf8');
      const jsonArchive: string = JSON.parse(content);
      archive = this.parseJson(jsonArchive);
    }

    if (ids) {
      return this.filterByID(archive, ids);
    }

    return archive;
  }

  public async set(data: Array<Item>): Promise<void> {
    try {
      const jsonData: string = JSON.stringify(data.map((item: Item) => item.toJSON()), null, 4);
      const tempStorageFile: string = this.getTempFile(this.mainStorageFile);

      fs.writeFileSync(tempStorageFile, jsonData, 'utf8');
      fs.renameSync(tempStorageFile, this.mainStorageFile);

      Promise.resolve();
    } catch (error) {
      Promise.reject(error);
    }
  }

  public async setArchive(archive: Array<Item>): Promise<void> {
    try {
      const jsonArchive: string = JSON.stringify(archive.map((item: Item) => item.toJSON()), null, 4);
      const tempArchiveFile: string = this.getTempFile(this.archiveFile);

      fs.writeFileSync(tempArchiveFile, jsonArchive, 'utf8');
      fs.renameSync(tempArchiveFile, this.archiveFile);

      Promise.resolve();
    } catch (error) {
      Promise.reject(error);
    }
  }

  private filterByID(data: Array<Item>, ids: Array<number>): Array<Item> {
    if (ids) {
      return data.filter(item => { return ids.indexOf(item.id) != -1; });
    }
    return data;
  }
}