From 439bafacfff205050d42dab7d995669f62950fc0 Mon Sep 17 00:00:00 2001 From: Stanislav Smetanin Date: Fri, 8 Aug 2025 23:12:09 +1000 Subject: [PATCH] Added possibility to export single file by using 'dw files {file path} {outPath} -e'. Added two optional options: --asDirectory - Forces the command to treat the path as a directory, even if its name contains a dot. --asFile - Forces the command to treat the path as a single file, even if it has no extension. They are needed in cases if automatic detection ('do we want to export the file or directory?') is incorrect, usually because of strange file/directory names. --- .gitignore | 5 +- README.md | 20 +++++++ bin/commands/files.js | 121 +++++++++++++++++++++++++++++++----------- 3 files changed, 113 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index bb6fa4d..09d90be 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,7 @@ dist .dynamodb/ # TernJS port file -.tern-port \ No newline at end of file +.tern-port + +# Visual Studio +/.vs \ No newline at end of file diff --git a/README.md b/README.md index 506dfa9..0f23971 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,26 @@ Exporting all templates from current environment to local solution Listing the system files structure of the current environment > $ dw files system -lr +### Files Source Type Detection +By default, the `dw files` command automatically detects the source type based on the \: +If the path contains a file extension (e.g., 'templates/Translations.xml'), it is treated as a file. +Otherwise, it is treated as a directory. +In cases where this detection is incorrect, you can force the type using these flags: + +- `-ad` `--asDirectory` Forces the command to treat the path as a directory, even if its name contains a dot. +- `-af` `--asFile` Forces the command to treat the path as a single file, even if it has no extension. + +#### Examples + +Exporting single file from current environment to local solution +> $ dw files templates/Translations.xml ./templates -e + +Exporting a directory that looks like a file +> $ dw files templates/templates.v1 ./templates -e -ad + +Exporting a file that has no extension +> $ dw files templates/testfile ./templates -e -af + ### Swift > $ dw swift \ diff --git a/bin/commands/files.js b/bin/commands/files.js index d917621..cbc0f07 100644 --- a/bin/commands/files.js +++ b/bin/commands/files.js @@ -63,6 +63,18 @@ export function filesCommand() { type: 'boolean', describe: 'Includes export of log and cache folders, NOT RECOMMENDED' }) + .option('asFile', { + type: 'boolean', + alias: 'af', + describe: 'Forces the command to treat the path as a single file, even if it has no extension.', + conflicts: 'asDirectory' + }) + .option('asDirectory', { + type: 'boolean', + alias: 'ad', + describe: 'Forces the command to treat the path as a directory, even if its name contains a dot.', + conflicts: 'asFile' + }) }, handler: (argv) => { if (argv.verbose) console.info(`Listing directory at: ${argv.dirPath}`) @@ -85,7 +97,19 @@ async function handleFiles(argv) { if (argv.export) { if (argv.dirPath) { - await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, []); + + const isFile = argv.asFile || argv.asDirectory + ? argv.asFile + : path.extname(argv.dirPath) !== ''; + + if (isFile) { + let parentDirectory = path.dirname(argv.dirPath); + parentDirectory = parentDirectory === '.' ? '/' : parentDirectory; + + await download(env, user, parentDirectory, argv.outPath, false, null, true, argv.iamstupid, [argv.dirPath], true); + } else { + await download(env, user, argv.dirPath, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false); + } } else { await interactiveConfirm('Are you sure you want a full export of files?', async () => { console.log('Full export is starting') @@ -93,9 +117,9 @@ async function handleFiles(argv) { let dirs = filesStructure.directories; for (let id = 0; id < dirs.length; id++) { const dir = dirs[id]; - await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, []); + await download(env, user, dir.name, argv.outPath, true, null, argv.raw, argv.iamstupid, [], false); } - await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name)); + await download(env, user, '/.', argv.outPath, false, 'Base.zip', argv.raw, argv.iamstupid, Array.from(filesStructure.files.data, f => f.name), false); if (argv.raw) console.log('The files in the base "files" folder is in Base.zip, each directory in "files" is in its own zip') }) } @@ -165,8 +189,7 @@ function resolveTree(dirs, indentLevel, parentHasFiles) { } } -async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames) { - let endpoint; +async function download(env, user, dirPath, outPath, recursive, outname, raw, iamstupid, fileNames, singleFileMode) { let excludeDirectories = ''; if (!iamstupid) { excludeDirectories = 'system/log'; @@ -174,19 +197,10 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia return; } } - let data = { - 'DirectoryPath': dirPath ?? '/', - 'ExcludeDirectories': [ excludeDirectories ], - } - if (recursive) { - endpoint = 'DirectoryDownload'; - } else { - endpoint = 'FileDownload' - data['Ids'] = fileNames - } + const { endpoint, data } = prepareDownloadCommandData(dirPath, excludeDirectories, fileNames, recursive, singleFileMode); - console.log('Downloading', dirPath === '/.' ? 'Base' : dirPath, 'Recursive=' + recursive); + displayDownloadMessage(dirPath, fileNames, recursive, singleFileMode); const res = await fetch(`${env.protocol}://${env.host}/Admin/Api/${endpoint}`, { method: 'POST', @@ -201,35 +215,78 @@ async function download(env, user, dirPath, outPath, recursive, outname, raw, ia const filename = outname || tryGetFileNameFromResponse(res, dirPath); if (!filename) return; - let filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) - let updater = createThrottledStatusUpdater(); + const filePath = path.resolve(`${path.resolve(outPath)}/${filename}`) + const updater = createThrottledStatusUpdater(); + await downloadWithProgress(res, filePath, { onData: (received) => { updater.update(`Received:\t${formatBytes(received)}`); } }); + updater.stop(); - console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); + if (singleFileMode) { + console.log(`Successfully downloaded: ${filename}`); + } else { + console.log(`Finished downloading`, dirPath === '/.' ? '.' : dirPath, 'Recursive=' + recursive); + } - if (!raw) { - console.log(`\nExtracting ${filename} to ${outPath}`); + await extractArchive(filename, filePath, outPath, raw); +} - const filenameWithoutExtension = filename.replace('.zip', ''); - const destinationPath = `${path.resolve(outPath)}/${filenameWithoutExtension === 'Base' ? '' : filenameWithoutExtension}`; +function prepareDownloadCommandData(directoryPath, excludeDirectories, fileNames, recursive, singleFileMode) { + const data = { + 'DirectoryPath': directoryPath ?? '/', + 'ExcludeDirectories': [excludeDirectories], + }; - updater = createThrottledStatusUpdater(); - await extractWithProgress(filePath, destinationPath, { - onEntry: (processedEntries, totalEntries, percent) => { - updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`); - } - }); - updater.stop(); + if (recursive && !singleFileMode) { + return { endpoint: 'DirectoryDownload', data }; + } + + data['Ids'] = fileNames; + return { endpoint: 'FileDownload', data }; +} + +function displayDownloadMessage(directoryPath, fileNames, recursive, singleFileMode) { + if (singleFileMode) { + const fileName = path.basename(fileNames[0] || 'unknown'); + console.log('Downloading file: ' + fileName); - console.log(`Finished extracting ${filename} to ${outPath}\n`); + return; + } + + const directoryPathDisplayName = directoryPath === '/.' + ? 'Base' + : directoryPath; + + console.log('Downloading', directoryPathDisplayName, 'Recursive=' + recursive); +} - fs.unlink(filePath, function(err) {}); +async function extractArchive(filename, filePath, outPath, raw) { + if (raw) { + return; } + + console.log(`\nExtracting ${filename} to ${outPath}`); + let destinationFilename = filename.replace('.zip', ''); + if (destinationFilename === 'Base') + destinationFilename = ''; + + const destinationPath = `${path.resolve(outPath)}/${destinationFilename}`; + const updater = createThrottledStatusUpdater(); + + await extractWithProgress(filePath, destinationPath, { + onEntry: (processedEntries, totalEntries, percent) => { + updater.update(`Extracted:\t${processedEntries} of ${totalEntries} files (${percent}%)`); + } + }); + + updater.stop(); + console.log(`Finished extracting ${filename} to ${outPath}\n`); + + fs.unlink(filePath, function(err) {}); } async function getFilesStructure(env, user, dirPath, recursive, includeFiles) {