UNPKG

3.91 kBPlain TextView Raw
1/**
2 * Copyright (c) Microsoft Corporation. All rights reserved.
3 * Licensed under the MIT License.
4 */
5/**
6 * @module botbuilder-node
7 */
8/**
9 * Copyright (c) Microsoft Corporation. All rights reserved.
10 * Licensed under the MIT License.
11 */
12import { Storage, StoreItems, StoreItem } from 'botbuilder';
13import * as path from 'path';
14import * as fs from 'async-file';
15import * as file from 'fs';
16import * as filenamify from 'filenamify';
17
18/**
19 * :package: **botbuilder**
20 *
21 * A file based storage provider. Items will be persisted to a folder on disk.
22 *
23 * **Usage Example**
24 *
25 * ```JavaScript
26 * const { FileStorage } = require('botbuilder');
27 * const path = require('path');
28 *
29 * const storage = new FileStorage(path.join(__dirname, './state'));
30 * ```
31 */
32export class FileStorage implements Storage {
33 static nextTag = 0;
34 private pEnsureFolder: Promise<void> | undefined
35 protected readonly path: string
36 /**
37 * Creates a new FileStorage instance.
38 * @param path Root filesystem path for where the provider should store its items.
39 */
40 public constructor(filePath: string) {
41 this.path = filePath
42 }
43
44 public async read(keys: string[]): Promise<StoreItems> {
45 await this.ensureFolder()
46 const data: StoreItems = {}
47 const promises: Promise<any>[] = keys.map(key => {
48 const filePath = this.getFilePath(key)
49 return parseFile(filePath)
50 .then((obj) => {
51 if (obj) {
52 data[key] = obj
53 }
54 })
55 })
56
57 await Promise.all(promises)
58 return data
59 }
60
61 public async write(changes: StoreItems): Promise<void> {
62 await this.ensureFolder()
63 const promises: Promise<void>[] = Object.keys(changes).map(key => {
64 const filePath = this.getFilePath(key)
65 return fs.exists(filePath)
66 .then((exists) => {
67 const newObj: StoreItem = { ...changes[key] }
68 newObj.eTag = (parseInt(newObj.eTag || '0', 10) + 1).toString()
69 return fs.writeTextFile(filePath, JSON.stringify(newObj))
70 })
71 })
72
73 await Promise.all(promises)
74 }
75
76 public async delete(keys: string[]): Promise<void> {
77 await this.ensureFolder()
78 const promises = keys.map(key => {
79 const filePath = this.getFilePath(key)
80 return fs.exists(filePath)
81 .then((exists) => {
82 if (exists) {
83 file.unlinkSync(filePath)
84 }
85 })
86 })
87
88 await Promise.all(promises)
89 }
90
91 private async ensureFolder(): Promise<void> {
92 if (this.pEnsureFolder) {
93 return this.pEnsureFolder;
94 }
95
96 const exists = await fs.exists(this.path)
97 if (!exists) {
98 this.pEnsureFolder = fs.mkdirp(this.path)
99 }
100 }
101
102 private getFileName(key: string): string {
103 return filenamify(key);
104 }
105
106 private getFilePath(key: string): string {
107 return path.join(this.path, this.getFileName(key));
108 }
109}
110
111async function parseFile(filePath: string): Promise<Object | undefined> {
112 try {
113 const exists = await fs.exists(filePath)
114 const data = await (exists
115 ? fs.readTextFile(filePath)
116 : Promise.resolve(undefined))
117 try {
118 if (data) {
119 return JSON.parse(data)
120 }
121 }
122 catch (err) {
123 console.warn(`FileStorage: error parsing "${filePath}": ${err.toString()}`)
124 }
125
126 return undefined
127 }
128 catch (error) {
129 // File could legitimately have been deleted
130 if (error.code != "ENOENT") {
131 console.warn(`FileStorage: error reading "${filePath}": ${error.toString()}`)
132 }
133 return undefined
134 }
135}