alfred-chrono-notes
Version:
Alfred Workflow for easy access to your Obsidian Periodic Notes
132 lines (131 loc) • 5.7 kB
JavaScript
import * as os from 'os';
import * as fs from 'fs';
import path from 'path';
import { InvalidFilePathSchemaException } from '../../Exceptions/InvalidFilePathSchemaException.js';
import { FileDoesNotExistException } from '../../Exceptions/FileDoesNotExistException.js';
import { PathNotFileException } from '../../Exceptions/PathNotFileException.js';
import { FileAlreadyExistsException } from '../../Exceptions/FileAlreadyExistsException.js';
import { FatalReadFileSyncException } from '../../Exceptions/FatalReadFileSyncException.js';
import { FatalWriteFileSyncException } from '../../Exceptions/FatalWriteFileSyncException.js';
/**
* @description This class implements the IFileProvider interface and provides functionality
* for resolving file paths, checking file existence, and creating templated files.
*
* It supports a single file provider driver type, but can add more.
*/
export class FileProvider {
// TODO: Support more FileProvider driver types as needed
/**
* Get the full path to a file using the correct date format.
* @param directoryPath - The provided relative path.
* @param formattedDate - The formatted date string.
* @returns - The full absolute path to a given file and formatted date string.
*/
resolveNoteFullPath(directoryPath, formattedDate) {
return `${path.join(this.resolveHomePath(directoryPath), formattedDate)}.md`;
}
/**
* A function that adds the entire path if not provided.
* @param filepath - The provided relative path.
* @returns - The full absolute path.
*/
resolveHomePath(filepath) {
if (filepath.startsWith('~') && process.env.HOME) {
return path.join(process.env.HOME, filepath.slice(1));
}
return filepath;
}
/**
* Check if path is valid.
*
* Provides a security check to prevent an attacker to execute arbitrary code.
* Checks the given path to make sure it's in the directory of the current OS user, and not any other or any system directories.
*
* Mitigates the ESlint rule security/detect-non-literal-fs-filename
* {@link https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-non-literal-fs-filename.md}.
* @param path - The provided path to check.
* @returns - True if the path is valid, false otherwise.
*/
isValidPathSchema(path) {
const userInfo = os.userInfo();
const regex = RegExp(`^(?=${userInfo.homedir})[a-zA-Z0-9._/ -]*$`);
return regex.test(path);
}
/**
* Function that checks if a file exists or not.
* @param filePath - The provided path to check.
* @returns - True if the file exists, false otherwise.
*/
doesFileExist(filePath) {
const path = this.resolveHomePath(filePath);
return fs.existsSync(path) && fs.statSync(path).isFile();
}
/**
* Check if file exists in vault.
* @param filePath - The path to the file.
* @throws {InvalidFilePathSchemaException} - If the provided path is not valid.
* @throws {FileDoesNotExistException} - If the provided path does not exist.
* @throws {PathNotFileException} - If the provided path is not a file.
*/
checkIfFileExists(filePath) {
const path = this.resolveHomePath(filePath);
if (!this.isValidPathSchema(path)) {
throw new InvalidFilePathSchemaException(`Invalid path schema: ${path}`);
}
if (!fs.existsSync(path)) {
throw new FileDoesNotExistException(`File does not exist at ${path}`);
}
if (!fs.statSync(path).isFile()) {
throw new PathNotFileException(`Path is not a file at ${path}`);
}
}
/**
* Reads a file and returns the content.
*
* A markdown (.md) file that's used to define a user's Obsidian template.
* @param filePath - The path to the file.
* @returns The contents of the file.
* @throws {Error} If the file cannot be read.
*/
readTemplate(filePath) {
let templateFileContent = '';
try {
templateFileContent = fs.readFileSync(filePath, 'utf8');
}
catch (_e) {
throw new FatalReadFileSyncException(`Could not read template file at ${filePath}`);
}
return templateFileContent;
}
/**
* Given a file path and valid templateFilePath, create a new file.
* @param filePath - The path to the new file.
* @param templateFilePath - The path to the template file.
* @throws {FileDoesNotExistException}
*/
createTemplatedNote(filePath, templateFilePath) {
// 1. Check that the provided filePath does not exist.
if (this.doesFileExist(filePath)) {
throw new FileAlreadyExistsException(`File already exists at ${filePath}`);
}
// 2. Check if templateFilePath given is actually a file
this.checkIfFileExists(templateFilePath);
let templateFileContent = '';
// 3. Fetch the template contents
const fullTemplateFilePath = this.resolveHomePath(templateFilePath);
try {
templateFileContent = this.readTemplate(fullTemplateFilePath);
}
catch (_e) {
throw new FatalReadFileSyncException(`Could not read template file at ${fullTemplateFilePath}`);
}
const fullFilePath = this.resolveHomePath(filePath);
// 4. Create new file from template
try {
fs.writeFileSync(fullFilePath, templateFileContent);
}
catch (_e) {
throw new FatalWriteFileSyncException(`Could not create templated file at ${filePath}`);
}
}
}