diff --git a/.env.example b/.env.example index 38e49456..7e88f158 100644 --- a/.env.example +++ b/.env.example @@ -40,3 +40,6 @@ PERMANENT_API_BASE_PATH=${LOCAL_TEMPORARY_AUTH_TOKEN} # See https://fusionauth.io/docs/v1/tech/apis/api-keys FUSION_AUTH_HOST=${FUSION_AUTH_HOST} FUSION_AUTH_KEY=${FUSION_AUTH_KEY} + +# See +LOG_LEVEL=debug diff --git a/package-lock.json b/package-lock.json index 9bb3d4a7..615cb058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "logform": "^2.3.2", "node-fetch": "^2.6.7", "ssh2": "^1.11.0", + "tmp": "^0.2.1", "uuid": "^8.3.2", "winston": "^3.4.0" }, @@ -28,6 +29,7 @@ "@tsconfig/node16": "^1.0.2", "@types/node-fetch": "^2.6.2", "@types/ssh2": "^1.11.4", + "@types/tmp": "^0.2.3", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", @@ -2938,6 +2940,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==", + "dev": true + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -3626,8 +3634,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", @@ -3650,7 +3657,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3937,8 +3943,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "node_modules/confusing-browser-globals": { "version": "1.0.11", @@ -5073,8 +5078,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "node_modules/fsevents": { "version": "2.3.2", @@ -5175,7 +5179,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5526,7 +5529,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -7976,7 +7978,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -8253,7 +8254,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "dependencies": { "wrappy": "1" } @@ -8371,7 +8371,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8813,7 +8812,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -9294,6 +9292,17 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -9793,8 +9802,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/write-file-atomic": { "version": "3.0.3", @@ -12006,6 +12014,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==", + "dev": true + }, "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -12486,8 +12500,7 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -12507,7 +12520,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12738,8 +12750,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "confusing-browser-globals": { "version": "1.0.11", @@ -13614,8 +13625,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.3.2", @@ -13685,7 +13695,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -13935,7 +13944,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -15755,7 +15763,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -15968,7 +15975,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -16058,8 +16064,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -16380,7 +16385,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "requires": { "glob": "^7.1.3" } @@ -16735,6 +16739,14 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -17125,8 +17137,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "3.0.3", diff --git a/package.json b/package.json index 60363564..122b9e12 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@tsconfig/node16": "^1.0.2", "@types/node-fetch": "^2.6.2", "@types/ssh2": "^1.11.4", + "@types/tmp": "^0.2.3", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", @@ -60,6 +61,7 @@ "logform": "^2.3.2", "node-fetch": "^2.6.7", "ssh2": "^1.11.0", + "tmp": "^0.2.1", "uuid": "^8.3.2", "winston": "^3.4.0" } diff --git a/src/classes/PermanentFileSystem.ts b/src/classes/PermanentFileSystem.ts index 10ffa0d9..50fc1804 100644 --- a/src/classes/PermanentFileSystem.ts +++ b/src/classes/PermanentFileSystem.ts @@ -2,10 +2,11 @@ import path from 'path'; import fs from 'fs'; import { createFolder, + createArchiveRecord, getArchives, getArchiveFolders, getFolder, - getRecord, + getArchiveRecord, } from '@permanentorg/sdk'; import { deduplicateFileEntries, @@ -18,12 +19,14 @@ import { getArchiveIdFromPath, getOriginalFileForRecord, } from '../utils'; +import { generateAttributesForFile } from '../utils/generateAttributesForFile'; +import type { Readable } from 'stream'; import type { Archive, ClientConfiguration, Folder, File, - Record, + ArchiveRecord, } from '@permanentorg/sdk'; import type { Attributes, @@ -61,7 +64,7 @@ export class PermanentFileSystem { private readonly archiveFoldersCache = new Map(); - private readonly recordCache = new Map(); + private readonly archiveRecordCache = new Map(); private archivesCache?: Archive[]; @@ -166,36 +169,61 @@ export class PermanentFileSystem { ); } + public async createFile( + requestedPath: string, + dataStream: Readable, + size: number, + ): Promise { + const parentPath = path.dirname(requestedPath); + const archiveRecordName = path.basename(requestedPath); + const parentFolder = await this.loadFolder(parentPath); + await createArchiveRecord( + this.getClientConfiguration(), + dataStream, + { + contentType: 'application/octet-stream', + size, + }, + { + displayName: archiveRecordName, + fileSystemCompatibleName: archiveRecordName, + }, + parentFolder, + ); + this.folderCache.delete(parentPath); + } + public async loadFile(requestedPath: string): Promise { if (!isItemPath(requestedPath)) { throw new Error('Invalid file path'); } const record = await this.loadRecord(requestedPath); + const archiveRecord = await this.loadArchiveRecord(requestedPath); return getOriginalFileForRecord(record); } - private async loadRecord(requestedPath: string): Promise { - const cachedRecord = this.recordCache.get(requestedPath); - if (cachedRecord) { - return cachedRecord; + private async loadArchiveRecord(requestedPath: string): Promise { + const cachedArchiveRecord = this.archiveRecordCache.get(requestedPath); + if (cachedArchiveRecord) { + return cachedArchiveRecord; } const parentPath = path.dirname(requestedPath); const childName = path.basename(requestedPath); - const parentFolder = await this.loadFolder(parentPath); + const parentFolder = await this.loadFolder(parentPath, true); const archiveId = getArchiveIdFromPath(parentPath); - const targetRecord = parentFolder.records.find( - (record) => record.fileSystemCompatibleName === childName, + const targetArchiveRecord = parentFolder.archiveRecords.find( + (archiveRecord) => archiveRecord.fileSystemCompatibleName === childName, ); - if (!targetRecord) { + if (!targetArchiveRecord) { throw new Error('This file does not exist'); } - const populatedRecord = await getRecord( + const populatedArchiveRecord = await getArchiveRecord( this.getClientConfiguration(), - targetRecord.id, + targetArchiveRecord.id, archiveId, ); - this.recordCache.set(requestedPath, populatedRecord); - return populatedRecord; + this.archiveRecordCache.set(requestedPath, populatedArchiveRecord); + return populatedArchiveRecord; } private async loadRecords(requestedPaths: string[]): Promise { @@ -261,14 +289,14 @@ export class PermanentFileSystem { return archiveFolders; } - private async loadFolder(requestedPath: string): Promise { + private async loadFolder(requestedPath: string, overrideCache = false): Promise { const cachedFolder = this.folderCache.get(requestedPath); - if (cachedFolder) { + if (cachedFolder && !overrideCache) { return cachedFolder; } if (!isItemPath(requestedPath)) { - throw new Error('The requested path cannot be a folder'); + throw new Error('The requested path is not a folder') } const parentPath = path.dirname(requestedPath); @@ -324,3 +352,4 @@ export class PermanentFileSystem { ]); } } +/// NEED TO GO THROUGH AND MAKE SURE ALL INSTANCES OF "RECORD" ARE NOW "ARCHIVERECORD" diff --git a/src/classes/SftpSessionHandler.ts b/src/classes/SftpSessionHandler.ts index fcc3fea3..1685b112 100644 --- a/src/classes/SftpSessionHandler.ts +++ b/src/classes/SftpSessionHandler.ts @@ -2,6 +2,8 @@ import path from 'path'; import fetch from 'node-fetch'; import { v4 as uuidv4 } from 'uuid'; import ssh2 from 'ssh2'; +import tmp from 'tmp'; +import type { FileResult } from 'tmp'; import { logger } from '../logger'; import { generateFileEntry } from '../utils'; import { PermanentFileSystem } from './PermanentFileSystem'; @@ -14,10 +16,52 @@ import type { File, } from '@permanentorg/sdk'; -const SFTP_STATUS_CODE = ssh2.utils.sftp.STATUS_CODE; +// This list SHOULD be replaced with the one defined in the ssh2 package +// from `ssh2.utils.sftp.STATUS_CODE` +// that list is currently incomplete, pending the merge of: +// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/63833 +// so we'll defined it here for now. +enum SFTP_STATUS_CODE { + OK = 0, + EOF = 1, + NO_SUCH_FILE = 2, + PERMISSION_DENIED = 3, + FAILURE = 4, + BAD_MESSAGE = 5, + NO_CONNECTION = 6, + CONNECTION_LOST = 7, + OP_UNSUPPORTED = 8, + INVALID_HANDLE = 9, + NO_SUCH_PATH = 10, + FILE_ALREADY_EXISTS = 11, + WRITE_PROTECT = 12, + NO_MEDIA = 13, + NO_SPACE_ON_FILESYSTEM = 14, + QUOTA_EXCEEDED = 15, + UNKNOWN_PRINCIPAL = 16, + LOCK_CONFLICT = 17, + DIR_NOT_EMPTY = 18, + NOT_A_DIRECTORY = 19, + INVALID_FILENAME = 20, + LINK_LOOP = 21, + CANNOT_DELETE = 22, + INVALID_PARAMETER = 23, + FILE_IS_A_DIRECTORY = 24, + BYTE_RANGE_LOCK_CONFLICT = 25, + BYTE_RANGE_LOCK_REFUSED = 26, + DELETE_PENDING = 27, + FILE_CORRUPT = 28, + OWNER_INVALID = 29, + GROUP_INVALID = 30, + NO_MATCHING_BYTE_RANGE_LOCK = 31, +} const generateHandle = (): string => uuidv4(); +interface TemporaryFile extends FileResult { + path: string; +} + export class SftpSessionHandler { private readonly sftpConnection: SFTPWrapper; @@ -25,6 +69,8 @@ export class SftpSessionHandler { private readonly openFiles: Map = new Map(); + private readonly openTemporaryFiles = new Map(); + private readonly permanentFileSystem: PermanentFileSystem; public constructor( @@ -43,7 +89,7 @@ export class SftpSessionHandler { */ public openHandler = ( reqId: number, - filename: string, + filePath: string, flags: number, attrs: Attributes, ): void => { @@ -51,31 +97,95 @@ export class SftpSessionHandler { 'Request: SFTP file open (SSH_FXP_OPEN)', { reqId, - filename, + filePath, flags, attrs, - }, + } ); const handle = generateHandle(); - logger.debug(`Opening ${filename}: ${handle}`); - this.permanentFileSystem.loadFile(filename) + const flagsString = ssh2.utils.sftp.flagsToString(flags) + logger.debug(`Opening file ${filePath}: ${handle} (${flagsString})`); + this.permanentFileSystem.loadFile(filePath) .then((file) => { logger.debug('Contents:', file); - this.openFiles.set(handle, file); - logger.verbose('Response: Handle', { reqId, handle }); - this.sftpConnection.handle( - reqId, - Buffer.from(handle), - ); + switch (flagsString) { + case 'r': + this.openFiles.set(handle, file); + logger.debug('Response:', { reqId, handle }); + this.sftpConnection.handle( + reqId, + Buffer.from(handle), + ); + break; + + // We do not currently allow anybody to edit an existing record in any way + case 'r+': + case 'w': + case 'w+': + case 'a': + case 'a+': + logger.debug( + 'Response: Status (PERMISSION_DENIED)', + { reqId }, + SFTP_STATUS_CODE.PERMISSION_DENIED, + ); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.PERMISSION_DENIED); + break; + + // These codes all require the file NOT to exist + case 'wx': + case 'xw': + case 'xw+': + case 'ax': + case 'xa': + case 'ax+': + case 'xa+': + default: + logger.verbose( + 'Response: Status (FILE_ALREADY_EXISTS)', + { reqId }, + SFTP_STATUS_CODE.FILE_ALREADY_EXISTS, + ); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.FILE_ALREADY_EXISTS); + break; + } }) - .catch((reason: unknown) => { - logger.warn('Failed to load file', { reqId, filename }); - logger.warn(reason); - logger.verbose('Response: Status (FAILURE)', { - reqId, - code: SFTP_STATUS_CODE.FAILURE, - }); - this.sftpConnection.status(reqId, SFTP_STATUS_CODE.FAILURE); + .catch(() => { + // Need to see if the file is actually a folder! + switch (flagsString) { + case 'r+': + case 'w': + case 'wx': + case 'xw': + case 'w+': + case 'xw+': + case 'ax': + case 'xa': + case 'a+': + case 'ax+': + case 'xa+': + const temporaryFile = tmp.fileSync(); + this.openTemporaryFiles.set(handle, { + ...temporaryFile, + path: filePath, + }); + logger.verbose('Response:', { reqId, handle }); + this.sftpConnection.handle( + reqId, + Buffer.from(handle), + ); + break; + case 'r': + case 'a': + default: + logger.verbose( + 'Response: Status (NO_SUCH_FILE)', + { reqId }, + SFTP_STATUS_CODE.NO_SUCH_FILE, + ); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.NO_SUCH_FILE); + break; + } }); }; @@ -165,9 +275,42 @@ export class SftpSessionHandler { * Also: Reading and Writing * https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.4 */ - // eslint-disable-next-line class-methods-use-this - public writeHandler = (): void => { - logger.error('UNIMPLEMENTED Request: SFTP write file (SSH_FXP_WRITE)'); + public writeHandler = ( + reqId: number, + handle: Buffer, + offset: number, + data: Buffer, + ): void => { + logger.verbose( + 'Request: SFTP write file (SSH_FXP_WRITE)', + { reqId, handle, offset }, + ); + logger.silly('Request Data:', data); + const temporaryFile = this.openTemporaryFiles.get(handle.toString()); + if (!temporaryFile) { + logger.info('There is no open temporary file associated with this handle', { reqId, handle }); + logger.verbose('Response: Status (FAILURE)', { reqId }, SFTP_STATUS_CODE.FAILURE); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.FAILURE); + return; + } + console.log("WRITING"); + fs.write( + temporaryFile.fd, + data, + 0, + (err, written, buffer) => { + if (err) { + logger.verbose('Response: Status (FAILURE)', { reqId, handle}, SFTP_STATUS_CODE.FAILURE); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.FAILURE); + return; + } + logger.debug('Successful Write.', { reqId, handle, written }); + logger.silly('Written Data:', { buffer }); + logger.verbose('Response: Status (OK)', { reqId }, SFTP_STATUS_CODE.OK); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.OK); + } + ) + console.log("WROTE"); }; /** @@ -184,6 +327,13 @@ export class SftpSessionHandler { 'Request: SFTP read open file statistics (SSH_FXP_FSTAT)', { reqId, itemPath }, ); + const file = this.openFiles.get(itemPath.toString()); + if (!file) { + logger.verbose('There is no open file associated with this path', { reqId, itemPath }); + logger.verbose('Response: Status (FAILURE)', { reqId }, SFTP_STATUS_CODE.FAILURE); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.FAILURE); + return; + } this.genericStatHandler(reqId, itemPath); }; @@ -209,6 +359,38 @@ export class SftpSessionHandler { 'Request: SFTP close file (SSH_FXP_CLOSE)', { reqId, handle }, ); + const temporaryFile = this.openTemporaryFiles.get(handle.toString()); + if (temporaryFile) { + fs.close(temporaryFile.fd); + const { size } = fs.statSync(temporaryFile.name); + this.permanentFileSystem.createFile( + temporaryFile.path, + fs.createReadStream(temporaryFile.name), + size, + ).then(() => { + temporaryFile.removeCallback(); + this.openTemporaryFiles.delete(handle.toString()); + logger.verbose( + 'Response: Status (OK)', + { + reqId, + code: SFTP_STATUS_CODE.OK, + }, + ); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.OK); + }).catch((err) => { + logger.verbose(err); + logger.verbose( + 'Response: Status (FAILURE)', + { + reqId, + code: SFTP_STATUS_CODE.FAILURE, + }, + ); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.FAILURE); + }); + return; + } this.openFiles.delete(handle.toString()); logger.verbose( 'Response: Status (OK)', @@ -232,7 +414,7 @@ export class SftpSessionHandler { { reqId, dirPath }, ); const handle = generateHandle(); - logger.debug(`Opening ${dirPath}:`, handle); + logger.debug(`Opening directory ${dirPath}:`, handle); this.permanentFileSystem.loadDirectory(dirPath) .then((fileEntries) => { logger.debug('Contents:', fileEntries); @@ -396,9 +578,17 @@ export class SftpSessionHandler { * Also: Setting File Attributes * https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-6.9 */ - // eslint-disable-next-line class-methods-use-this - public setStatHandler = (): void => { - logger.error('UNIMPLEMENTED Request: SFTP set file attributes (SSH_FXP_SETSTAT)'); + public setStatHandler = ( + reqId: number, + path: string, + attrs: Attributes, + ): void => { + logger.verbose( + 'Request: SFTP set file attributes request (SSH_FXP_SETSTAT)', + { reqId, path, attrs }, + ); + logger.verbose('Response: Status (OK)', { reqId }, SFTP_STATUS_CODE.OK); + this.sftpConnection.status(reqId, SFTP_STATUS_CODE.OK); }; /** @@ -413,7 +603,7 @@ export class SftpSessionHandler { attrs: Attributes, ): void => { logger.verbose( - 'Request: FTP create directory (SSH_FXP_MKDIR)', + 'Request: SFTP create directory (SSH_FXP_MKDIR)', { reqId, dirPath, attrs }, ); this.permanentFileSystem.makeDirectory(dirPath) diff --git a/src/utils/getOriginalFile.ts b/src/utils/getOriginalFile.ts new file mode 100644 index 00000000..e61d9104 --- /dev/null +++ b/src/utils/getOriginalFile.ts @@ -0,0 +1,15 @@ +import { DerivativeType } from '@permanentorg/sdk'; +import type { + File, + Item, +} from '@permanentorg/sdk'; + +export const getOriginalFile = (record: Item): File => { + const originalFile = record.files.find( + (file) => file.derivativeType === DerivativeType.Original, + ); + if (!originalFile) { + throw Error('Permanent does not have an original file for this record'); + } + return originalFile; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 64cbfe3e..4b1640f7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,4 +7,8 @@ export * from './generateFileEntriesForRecords'; export * from './generateFileEntry'; export * from './getArchiveIdFromPath'; export * from './getLongname'; +<<<<<<< HEAD export * from './getOriginalFileForRecord'; +======= +export * from './getOriginalFile'; +>>>>>>> 8fd5e9a (WIP for file creation)