contact
Version:
private, one-to-one or many-to-many command line chat
116 lines (105 loc) • 3.41 kB
JavaScript
import { promises as fs, createReadStream, createWriteStream } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { performance } from 'node:perf_hooks'
import Transmission from 'contact-model/transmission'
import { streamReadText } from 'stream-read-all'
import { Readable } from 'node:stream'
const configDir = path.join(os.homedir(), '.contact')
const roomsDir = path.join(configDir, 'rooms')
const historyPath = path.join(configDir, 'history')
class Storage {
existingPaths = new Set()
async init () {
try {
await fs.mkdir(configDir)
} catch (err) {
if (err.code !== 'EEXIST') {
throw err
}
}
}
async writeHistory (history) {
history = history.filter(h => h.startsWith('/'))
await fs.writeFile(historyPath, history.join('\n'))
}
async readHistory () {
try {
const history = await fs.readFile(historyPath, { encoding: 'utf8' })
return history.split('\n')
} catch (err) {
if (err.code === 'ENOENT') {
return []
} else {
throw err
}
}
}
async createDir (dirPath) {
if (!this.existingPaths.has(dirPath)) {
await fs.mkdir(dirPath, { recursive: true })
this.existingPaths.add(dirPath)
}
}
async writeUnread (t) {
const roomId = t.headers['room-id']
if (roomId) {
const promises = []
const roomDirPath = path.join(roomsDir, roomId)
await this.createDir(roomDirPath)
const fileId = performance.now() * 1e6
const headerFilePath = path.join(roomDirPath, `${fileId}.json`)
promises.push(fs.writeFile(headerFilePath, JSON.stringify(t.headers)))
if (Number(t.headers['content-length']) > 0) {
const bodyFilePath = path.join(roomDirPath, `${fileId}`)
const prom = new Promise((resolve, reject) => {
const stream = t.readable.pipe(createWriteStream(bodyFilePath))
stream.on('close', resolve)
stream.on('error', reject)
})
promises.push(prom)
}
return Promise.all(promises)
} else {
throw new Error('Room transmissions only')
}
}
async io (fn) {
try {
const result = await fn()
return result
} catch (err) {
if (err.code === 'ENOENT') {
return null
} else {
throw new Error('I/O failed', { cause: err })
}
}
}
async readUnread (roomId) {
const roomDirPath = path.join(roomsDir, roomId)
const roomDirFiles = await this.io(() => fs.readdir(roomDirPath))
if (roomDirFiles === null) {
return []
}
const headerFiles = roomDirFiles.sort().filter(f => /\.json/.test(f))
const transmissions = []
for (const file of headerFiles) {
const headersPath = path.join(roomsDir, roomId, file)
const headers = JSON.parse(await fs.readFile(headersPath))
await fs.rm(headersPath)
const readablePath = path.join(roomsDir, roomId, path.basename(file, '.json'))
const stat = await this.io(() => fs.stat(readablePath))
if (stat === null) {
transmissions.push(new Transmission(headers))
} else {
const readable = createReadStream(readablePath)
const msg = await streamReadText(readable)
transmissions.push(new Transmission(headers, Readable.from([msg])))
await fs.rm(readablePath)
}
}
return transmissions
}
}
export default Storage