| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- import minimist from 'minimist'
- import fs from 'node:fs/promises'
- import Path from 'node:path'
- import readline from 'node:readline/promises'
- import { stdin as input, stdout as output } from 'node:process'
- import { ObjectId } from '../app/src/infrastructure/mongodb.mjs'
- import Errors from '../app/src/Features/Errors/Errors.js'
- import ProjectLocator from '../app/src/Features/Project/ProjectLocator.mjs'
- import ProjectEntityUpdateHandler from '../app/src/Features/Project/ProjectEntityUpdateHandler.mjs'
- import SafePath from '../app/src/Features/Project/SafePath.mjs'
- import { scriptRunner } from './lib/ScriptRunner.mjs'
- function usage() {
- console.error(`Upload a local file into a project path
- Usage: node scripts/upload_file.mjs [options] FILE
- Required:
- FILE Local filesystem path to file to upload
- --project-id ID Project id
- --user-id ID User id performing the action
- Optional:
- --dest PATH Destination project path (default: basename of FILE)
- --source VALUE Source label for history
- (default: script-upload-file)
- --force Allow overwrite when destination already exists
- -y Skip interactive confirmation prompt
- --dry-run Show what would happen without mutating the project
- --help Show this help
- Example:
- node scripts/upload_file.mjs /tmp/plot.png --project-id=... --user-id=... \
- --dest=/figures/plot.png --dry-run`)
- }
- function parseArgs() {
- let unknownArg
- const argv = minimist(process.argv.slice(2), {
- boolean: ['dry-run', 'force', 'help', 'y'],
- string: ['project-id', 'user-id', 'dest', 'source'],
- alias: { y: 'yes' },
- unknown: arg => {
- if (arg.startsWith('-')) {
- unknownArg = arg
- return false
- }
- return true
- },
- })
- if (unknownArg) {
- throw new Error(`unknown argument: ${unknownArg}`)
- }
- if (argv._.length === 0) {
- throw new Error('provide a local file path as FILE argument')
- }
- if (argv._.length > 1) {
- throw new Error('only one FILE argument is supported')
- }
- return {
- localPath: argv._[0],
- projectId: argv['project-id'],
- userId: argv['user-id'],
- destPath: argv.dest,
- source: argv.source || 'script-upload-file',
- force: argv.force === true,
- dryRun: argv['dry-run'] === true,
- assumeYes: argv.y === true || argv.yes === true,
- help: argv.help === true,
- }
- }
- function normalizeTargetPath(targetPath) {
- if (typeof targetPath !== 'string') {
- throw new TypeError('destination path must be a string')
- }
- if (targetPath.trim().length === 0) {
- throw new Error('destination path must not be empty')
- }
- return targetPath.startsWith('/') ? targetPath : `/${targetPath}`
- }
- async function validateInputs(opts) {
- const { projectId, userId, localPath } = opts
- if (!projectId || !ObjectId.isValid(projectId)) {
- throw new Error('provide a valid object id as --project-id')
- }
- if (!userId || !ObjectId.isValid(userId)) {
- throw new Error('provide a valid object id as --user-id')
- }
- if (!localPath || typeof localPath !== 'string') {
- throw new Error('provide a local file path as FILE argument')
- }
- let fileStat
- try {
- fileStat = await fs.stat(localPath)
- } catch (error) {
- throw new Error(`local file not found: ${localPath}`)
- }
- if (!fileStat.isFile()) {
- throw new Error(`local path is not a file: ${localPath}`)
- }
- const rawDestPath = opts.destPath ?? Path.basename(localPath)
- let targetPath
- try {
- targetPath = normalizeTargetPath(rawDestPath)
- } catch (error) {
- const invalidValue =
- opts.destPath !== undefined
- ? `--dest=${JSON.stringify(opts.destPath)}`
- : `derived basename ${rawDestPath} from FILE ${localPath}`
- throw new Error(
- `provide a non-empty destination project path; invalid value ${invalidValue}`
- )
- }
- if (!SafePath.isCleanPath(targetPath)) {
- throw new Errors.InvalidNameError('invalid --dest value')
- }
- const fileName = Path.posix.basename(targetPath)
- if (!fileName || fileName === '.' || fileName === '..') {
- throw new Error('destination path must include a file name')
- }
- return { ...opts, targetPath }
- }
- async function confirmUpload(projectId, localPath, targetPath, assumeYes) {
- if (assumeYes) {
- return true
- }
- const rl = readline.createInterface({ input, output })
- try {
- const answer = await rl.question(
- `Upload ${localPath} to ${targetPath} in project ${projectId}? [y/N] `
- )
- return /^y(es)?$/i.test(answer.trim())
- } finally {
- rl.close()
- }
- }
- async function getExistingEntity(projectId, targetPath) {
- try {
- return await ProjectLocator.promises.findElementByPath({
- project_id: projectId,
- path: targetPath,
- exactCaseMatch: true,
- })
- } catch (error) {
- if (error instanceof Errors.NotFoundError) {
- return null
- }
- throw error
- }
- }
- async function main(trackProgress) {
- let opts = parseArgs()
- if (opts.help) {
- usage()
- return
- }
- opts = await validateInputs(opts)
- const {
- projectId,
- userId,
- targetPath,
- localPath,
- source,
- force,
- dryRun,
- assumeYes,
- } = opts
- await trackProgress(
- `Starting upload for project=${projectId} path=${targetPath} dryRun=${dryRun} force=${force}`
- )
- const existing = await getExistingEntity(projectId, targetPath)
- if (existing?.type === 'folder') {
- throw new Error(
- `destination is a folder at ${targetPath}. Choose a file path within that folder.`
- )
- }
- if (existing && !force) {
- throw new Error(
- `destination already exists at ${targetPath} (type=${existing.type}). Re-run with --force to overwrite.`
- )
- }
- const intendedAction = existing ? 'overwrite' : 'create'
- console.log(
- `${dryRun ? 'DRY RUN: would' : 'Applying: will'} ${intendedAction} file at ${targetPath}`
- )
- if (dryRun) {
- return
- }
- const confirmed = await confirmUpload(
- projectId,
- localPath,
- targetPath,
- assumeYes
- )
- if (!confirmed) {
- console.log('Upload cancelled.')
- return
- }
- const { fileRef, isNew } =
- await ProjectEntityUpdateHandler.promises.upsertFileWithPath(
- projectId,
- targetPath,
- localPath,
- null,
- userId,
- source
- )
- const outcome = existing
- ? `overwrote existing ${existing.type}`
- : isNew
- ? 'created file'
- : 'updated existing file'
- console.log(
- `Success: ${outcome}. fileId=${fileRef?._id} fileName=${fileRef?.name} projectId=${projectId} path=${targetPath}`
- )
- }
- try {
- await scriptRunner(main)
- console.log('Done.')
- process.exit(0)
- } catch (error) {
- if (error instanceof Errors.InvalidNameError) {
- console.error(`Invalid name/path: ${error.message}`)
- } else {
- console.error(error)
- }
- usage()
- process.exit(1)
- }
|