Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/sync-client/lib/client-fs.ts
Views: 687
import type { ClientFs as ClientFsType } from "@cocalc/sync/client/types";1import Client, { Role } from "./index";2import ensureContainingDirectoryExists from "@cocalc/backend/misc/ensure-containing-directory-exists";3import { join } from "node:path";4import { readFile, writeFile, stat as statFileAsync } from "node:fs/promises";5import { exists, stat } from "fs";6import fs from "node:fs";7import type { CB } from "@cocalc/util/types/callback";8import { Watcher } from "@cocalc/backend/watcher";910import getLogger from "@cocalc/backend/logger";1112const logger = getLogger("sync-client:client-fs");1314export class ClientFs extends Client implements ClientFsType {15private filesystemClient = new FileSystemClient();1617write_file = this.filesystemClient.write_file;18path_read = this.filesystemClient.path_read;19path_stat = this.filesystemClient.path_stat;20path_exists = this.filesystemClient.path_exists;21file_size_async = this.filesystemClient.file_size_async;22file_stat_async = this.filesystemClient.file_stat_async;23watch_file = this.filesystemClient.watch_file;24path_access = this.filesystemClient.path_access;2526constructor({27project_id,28client_id,29home,30role,31}: {32project_id: string;33client_id?: string;34home?: string;35role: Role;36}) {37super({ project_id, client_id, role });38this.filesystemClient.setHome(home ?? process.env.HOME ?? "/home/user");39}40}4142// Some functions for reading and writing files under node.js43// where the read and write is aware of other reading and writing,44// motivated by the needs of realtime sync.45export class FileSystemClient {46private _file_io_lock?: { [key: string]: number }; // file → timestamps47private home: string;4849constructor() {50this.home = process.env.HOME ?? "/home/user";51}5253setHome(home: string) {54this.home = home;55}5657// Write a file to a given path (relative to this.home) on disk; will create containing directory.58// If file is currently being written or read in this process, will result in error (instead of silently corrupt data).59// WARNING: See big comment below for path_read.60write_file = async (opts: {61path: string;62data: string;63cb: CB<void>;64}): Promise<void> => {65// WARNING: despite being async, this returns nothing!66const path = join(this.home, opts.path);67if (this._file_io_lock == null) {68this._file_io_lock = {};69}70logger.debug("write_file", path);71const now = Date.now();72if (now - (this._file_io_lock[path] ?? 0) < 15000) {73// lock automatically expires after 15 seconds (see https://github.com/sagemathinc/cocalc/issues/1147)74logger.debug("write_file", path, "LOCK");75// Try again in about 1s.76setTimeout(() => this.write_file(opts), 500 + 500 * Math.random());77return;78}79logger.debug("write_file", "file_io_lock", this._file_io_lock);80try {81this._file_io_lock[path] = now;82logger.debug(path, "write_file -- ensureContainingDirectoryExists");83await ensureContainingDirectoryExists(path);84logger.debug(path, "write_file -- actually writing it to disk");85await writeFile(path, opts.data);86logger.debug("write_file", "success");87opts.cb();88} catch (error) {89const err = error;90logger.debug("write_file", "error", err);91opts.cb(err);92} finally {93if (this._file_io_lock != null) {94delete this._file_io_lock[path];95}96}97};9899// Read file as a string from disk.100// If file is currently being written or read in this process,101// will retry until it isn't, so we do not get an error and we102// do NOT get silently corrupted data.103// TODO and HUGE AWARNING: Despite this function being async, it DOES NOT104// RETURN ANYTHING AND DOES NOT THROW EXCEPTIONS! Just use it like any105// other old cb function. Todo: rewrite this and anything that uses it.106// This is just a halfway step toward rewriting project away from callbacks and coffeescript.107path_read = async (opts: {108path: string;109maxsize_MB?: number; // in megabytes; if given and file would be larger than this, then cb(err)110cb: CB<string>; // cb(err, file content as string (not Buffer!))111}): Promise<void> => {112// WARNING: despite being async, this returns nothing!113let content: string | undefined = undefined;114const path = join(this.home, opts.path);115logger.debug(`path_read(path='${path}', maxsize_MB=${opts.maxsize_MB})`);116if (this._file_io_lock == null) {117this._file_io_lock = {};118}119120const now = Date.now();121if (now - (this._file_io_lock[path] ?? 0) < 15000) {122// lock expires after 15 seconds (see https://github.com/sagemathinc/cocalc/issues/1147)123logger.debug(`path_read(path='${path}')`, "LOCK");124// Try again in 1s.125setTimeout(126async () => await this.path_read(opts),127500 + 500 * Math.random(),128);129return;130}131try {132this._file_io_lock[path] = now;133134logger.debug(135`path_read(path='${path}')`,136"_file_io_lock",137this._file_io_lock,138);139140// checking filesize limitations141if (opts.maxsize_MB != null) {142logger.debug(`path_read(path='${path}')`, "check if file too big");143let size: number | undefined = undefined;144try {145size = await this.file_size_async(path);146} catch (err) {147logger.debug("error checking", err);148opts.cb(err);149return;150}151152if (size > opts.maxsize_MB * 1000000) {153logger.debug(path, "file is too big!");154opts.cb(155new Error(156`file '${path}' size (=${157size / 1000000158}MB) too large (must be at most ${159opts.maxsize_MB160}MB); try opening it in a Terminal with vim instead or click Help in the upper right to open a support request`,161),162);163return;164} else {165logger.debug(path, "file is fine");166}167}168169// if the above passes, actually reading file170171try {172const data = await readFile(path);173logger.debug(path, "read file");174content = data.toString();175} catch (err) {176logger.debug(path, "error reading file", err);177opts.cb(err);178return;179}180} finally {181// release lock182if (this._file_io_lock) {183delete this._file_io_lock[path];184}185}186187opts.cb(undefined, content);188};189190file_size_async = async (filename: string) => {191const stat = await this.file_stat_async(filename);192return stat.size;193};194195file_stat_async = async (filename: string) => {196return await statFileAsync(filename);197};198199path_stat = (opts: { path: string; cb: CB }) => {200// see https://nodejs.org/api/fs.html#fs_class_fs_stats201const path = join(this.home, opts.path);202stat(path, opts.cb);203};204205path_exists = (opts: { path: string; cb: CB }) => {206const path = join(this.home, opts.path);207exists(path, (exists) => {208opts.cb(undefined, exists);209});210};211212watch_file = ({213path: relPath,214// don't fire until at least this many ms after the file has REMAINED UNCHANGED215debounce,216}: {217path: string;218debounce?: number;219}): Watcher => {220const path = join(this.home, relPath);221logger.debug("watching file", { path, debounce });222return new Watcher(path, { debounce });223};224225is_deleted = (_path: string, _project_id: string) => {226// not implemented yet in general227return undefined;228};229230set_deleted = (_path: string, _project_id?: string) => {231// TODO: this should edit the listings232};233234path_access = (opts: { path: string; mode: string; cb: CB }) => {235// mode: sub-sequence of 'rwxf' -- see https://nodejs.org/api/fs.html#fs_class_fs_stats236// cb(err); err = if any access fails; err=undefined if all access is OK237const path = join(this.home, opts.path);238let access = 0;239for (let s of opts.mode) {240access |= fs[s.toUpperCase() + "_OK"];241}242fs.access(path, access, opts.cb);243};244}245246247