From bd6c7a8db3befb79dde0f9dfe2505b66d73b3877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Srokosz?= Date: Thu, 20 Nov 2025 19:27:11 +0100 Subject: [PATCH] Feature: Download raw analysis files from web (#1148) --- dev/compose.yml | 2 + drakrun/web/api.py | 21 ++ drakrun/web/frontend/package-lock.json | 129 +++++++++++- drakrun/web/frontend/package.json | 3 + drakrun/web/frontend/src/AnalysisFiles.jsx | 214 ++++++++++++++++++++ drakrun/web/frontend/src/AnalysisReport.jsx | 4 + drakrun/web/frontend/src/App.css | 5 + drakrun/web/frontend/src/api.js | 15 ++ drakrun/web/frontend/vite.config.js | 4 + drakrun/web/schema.py | 4 + drakrun/web/storage.py | 25 +++ 11 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 drakrun/web/frontend/src/AnalysisFiles.jsx diff --git a/dev/compose.yml b/dev/compose.yml index c66b0d78..841bb4d1 100644 --- a/dev/compose.yml +++ b/dev/compose.yml @@ -14,5 +14,7 @@ services: build: context: .. dockerfile: ./dev/frontend.Dockerfile + volumes: + - "../drakrun/web/frontend/src:/app/src" ports: - "3000:3000" diff --git a/drakrun/web/api.py b/drakrun/web/api.py index 01eb1463..d6ad42b5 100644 --- a/drakrun/web/api.py +++ b/drakrun/web/api.py @@ -28,6 +28,7 @@ from drakrun.lib.config import load_config from drakrun.lib.paths import UPLOADS_DIR from drakrun.lib.s3_storage import get_s3_client, is_s3_enabled, upload_sample_to_s3 from drakrun.web.schema import ( + AnalysisFileRequestQuery, AnalysisListResponse, AnalysisRequestPath, AnalysisResponse, @@ -40,6 +41,7 @@ from drakrun.web.schema import ( UploadFileForm, ) from drakrun.web.storage import ( + list_analysis_files, list_analysis_logs, open_seekable_stream, read_analysis_json, @@ -307,6 +309,25 @@ def list_logs(path: AnalysisRequestPath): return jsonify(analysis_logs) +@api.get("/files/") +def list_files(path: AnalysisRequestPath): + task_uid = path.task_uid + analysis_files = list_analysis_files(task_uid, s3_config=config.s3) + return jsonify(analysis_files) + + +@api.get("/files//download") +def download_analysis_file(path: AnalysisRequestPath, query: AnalysisFileRequestQuery): + task_uid = path.task_uid + return send_analysis_file( + task_uid, + query.filename, + s3_config=config.s3, + mimetype="application/octet-stream", + download_name=query.filename.split("/")[-1], + ) + + @api.get("/graph/") def graph(path: AnalysisRequestPath): task_uid = path.task_uid diff --git a/drakrun/web/frontend/package-lock.json b/drakrun/web/frontend/package-lock.json index 0b7e34b8..0df1d85b 100644 --- a/drakrun/web/frontend/package-lock.json +++ b/drakrun/web/frontend/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@drakvuf-sandbox/web", - "version": "0.19.0", + "version": "0.20.0", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", @@ -16,10 +16,13 @@ "@novnc/novnc": "^1.5.0", "axios": "^1.12.0", "bootstrap": "^5.3.5", + "jszip": "^3.10.1", + "jszip-utils": "^0.1.0", "prettier": "^3.5.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-medium-image-zoom": "^5.2.14", + "react-modal": "^3.16.3", "react-router-dom": "^6.30.0", "react-select": "^5.10.1", "startbootstrap-sb-admin": "^7.0.7" @@ -1803,6 +1806,11 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -2180,6 +2188,11 @@ "node": ">=0.10.0" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2471,6 +2484,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2495,6 +2513,11 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2535,6 +2558,11 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2605,6 +2633,22 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/jszip-utils/-/jszip-utils-0.1.0.tgz", + "integrity": "sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg==" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2627,6 +2671,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -2812,6 +2864,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2940,6 +2997,11 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2988,6 +3050,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-medium-image-zoom": { "version": "5.2.14", "resolved": "https://registry.npmjs.org/react-medium-image-zoom/-/react-medium-image-zoom-5.2.14.tgz", @@ -3003,6 +3070,21 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -3085,6 +3167,20 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -3156,6 +3252,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -3170,6 +3271,11 @@ "semver": "bin/semver.js" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3234,6 +3340,14 @@ "@popperjs/core": "^2.11.6" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3355,6 +3469,11 @@ } } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/virtua": { "version": "0.40.3", "resolved": "https://registry.npmjs.org/virtua/-/virtua-0.40.3.tgz", @@ -3459,6 +3578,14 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/drakrun/web/frontend/package.json b/drakrun/web/frontend/package.json index 7a2ccb0c..5efb10ef 100644 --- a/drakrun/web/frontend/package.json +++ b/drakrun/web/frontend/package.json @@ -18,10 +18,13 @@ "@novnc/novnc": "^1.5.0", "axios": "^1.12.0", "bootstrap": "^5.3.5", + "jszip": "^3.10.1", + "jszip-utils": "^0.1.0", "prettier": "^3.5.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-medium-image-zoom": "^5.2.14", + "react-modal": "^3.16.3", "react-router-dom": "^6.30.0", "react-select": "^5.10.1", "startbootstrap-sb-admin": "^7.0.7" diff --git a/drakrun/web/frontend/src/AnalysisFiles.jsx b/drakrun/web/frontend/src/AnalysisFiles.jsx new file mode 100644 index 00000000..50a40776 --- /dev/null +++ b/drakrun/web/frontend/src/AnalysisFiles.jsx @@ -0,0 +1,214 @@ +import { use, useCallback, useEffect, useRef, useState } from "react"; +import { getAnalysisFileList } from "./api.js"; +import { CanceledError } from "axios"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faDownload, faFile } from "@fortawesome/free-solid-svg-icons"; +import Modal from "react-modal"; +import JSZip from "jszip"; +import JSZipUtils from "jszip-utils"; + +let BASE_URL = ""; +if (import.meta.env.VITE_API_SERVER) { + BASE_URL = import.meta.env.VITE_API_SERVER; +} else { + BASE_URL = "/api"; +} + +Modal.setAppElement("#root"); + +function urlToPromise(url) { + return new Promise(function (resolve, reject) { + JSZipUtils.getBinaryContent(url, function (err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +function FileTree({ fileList, analysisId }) { + return ( +
    + {fileList.map((file, idx) => { + return ( +
  • + + + + {file} + + +
  • + ); + })} +
+ ); +} + +export function AnalysisZipDownload({ files, analysisId }) { + const [modalOpened, setModalOpened] = useState(false); + const [modalMessage, setModalMessage] = useState(""); + const [downloadFinished, setDownloadFinished] = useState(false); + const [blobURL, setBlobURL] = useState(null); + const modalStyle = { + content: { + top: "50%", + left: "50%", + right: "auto", + bottom: "auto", + marginRight: "-50%", + transform: "translate(-50%, -50%)", + maxHeight: "100%", + }, + }; + + const makeZip = useCallback(() => { + const zip = new JSZip(); + for (let file of files) { + zip.file( + file, + urlToPromise( + `${BASE_URL}/files/${analysisId}/download?filename=${encodeURIComponent(file)}`, + ), + { binary: true }, + ); + } + zip.generateAsync({ type: "blob" }, (metadata) => { + let msg = "progression : " + metadata.percent.toFixed(2) + " %"; + if (metadata.currentFile) { + msg += ", current file = " + metadata.currentFile; + } + setModalMessage(msg); + }).then( + (blob) => { + setBlobURL(URL.createObjectURL(blob)); + setModalMessage(""); + setDownloadFinished(true); + }, + (err) => { + setModalMessage(err); + setDownloadFinished(true); + }, + ); + }, [files, analysisId]); + + const startDownload = useCallback(() => { + setDownloadFinished(false); + setModalOpened(true); + makeZip(); + }, [makeZip]); + + const closeModal = useCallback(() => { + if (blobURL) { + URL.revokeObjectURL(blobURL); + setBlobURL(null); + } + setModalOpened(false); + }, [blobURL]); + + return ( + <> + +
+ {downloadFinished ? ( + blobURL ? ( +
+ + Analysis downloaded successfully. + +
+ Click on the link below to store it: +
+ ) : ( +
Download failed
+ ) + ) : ( +
Downloading analysis files...
+ )} + + {modalMessage} + {downloadFinished ? ( + <> + {blobURL ? ( + + ) : ( + [] + )} + + ) : ( + [] + )} +
+
+
+ +
+ + ); +} + +export function AnalysisFilesViewer({ analysisId }) { + const [files, setFiles] = useState(); + const [error, setError] = useState(); + + useEffect(() => { + const abortController = new AbortController(); + setFiles(undefined); + getAnalysisFileList({ analysisId, abortController }) + .then((response) => { + setFiles(response.slice().sort()); + }) + .catch((error) => { + if (!(error instanceof CanceledError)) { + setError(error); + console.error(error); + } + }); + return () => { + abortController.abort(); + }; + }, [analysisId]); + + if (typeof error !== "undefined") { + return
Error: {error.toString()}
; + } + + if (typeof files === "undefined") { + return
Loading analysis files list...
; + } + + return ( +
+
+ + +
+
+ ); +} diff --git a/drakrun/web/frontend/src/AnalysisReport.jsx b/drakrun/web/frontend/src/AnalysisReport.jsx index d024c85a..3c76586d 100644 --- a/drakrun/web/frontend/src/AnalysisReport.jsx +++ b/drakrun/web/frontend/src/AnalysisReport.jsx @@ -18,6 +18,7 @@ import { faDownload } from "@fortawesome/free-solid-svg-icons"; import axios, { CanceledError } from "axios"; import { AnalysisSummary } from "./AnalysisSummary.jsx"; import { ProcessBadge } from "./ProcessBadge.jsx"; +import { AnalysisFilesViewer } from "./AnalysisFiles.jsx"; export function AnalysisLogViewerTab({ analysisId }) { const logType = useTabContext()?.activeTab; @@ -269,6 +270,9 @@ function AnalysisReportTabs({ ) : null} + + + diff --git a/drakrun/web/frontend/src/App.css b/drakrun/web/frontend/src/App.css index 6fc51638..0a32296f 100644 --- a/drakrun/web/frontend/src/App.css +++ b/drakrun/web/frontend/src/App.css @@ -69,3 +69,8 @@ li.selected { color: #6c757d; } /* Ends overriding */ + +.ReactModalPortal { + z-index: 10000; + position: absolute; +} diff --git a/drakrun/web/frontend/src/api.js b/drakrun/web/frontend/src/api.js index 6447db93..664e7a82 100644 --- a/drakrun/web/frontend/src/api.js +++ b/drakrun/web/frontend/src/api.js @@ -103,6 +103,21 @@ export async function getProcessLog({ return logRequest.data; } +export async function getAnalysisFileList({ + analysisId, + abortController = undefined, +}) { + const listRequest = await axios.get( + `/files/${analysisId}`, + abortController + ? { + signal: abortController.signal, + } + : {}, + ); + return listRequest.data; +} + export async function uploadSample({ file, timeout, diff --git a/drakrun/web/frontend/vite.config.js b/drakrun/web/frontend/vite.config.js index 0e23ccf2..f108a00e 100644 --- a/drakrun/web/frontend/vite.config.js +++ b/drakrun/web/frontend/vite.config.js @@ -18,6 +18,10 @@ export default defineConfig({ target: process.env.PROXY_BACKEND_URL, changeOrigin: true, }, + "/openapi": { + target: process.env.PROXY_BACKEND_URL, + changeOrigin: true, + } } : {} ), } diff --git a/drakrun/web/schema.py b/drakrun/web/schema.py index ce6a73fb..625a9767 100644 --- a/drakrun/web/schema.py +++ b/drakrun/web/schema.py @@ -57,6 +57,10 @@ class AnalysisRequestPath(BaseModel): ) +class AnalysisFileRequestQuery(BaseModel): + filename: str + + class LogsRequestPath(AnalysisRequestPath): log_type: str diff --git a/drakrun/web/storage.py b/drakrun/web/storage.py index dce70dee..ceddb4e7 100644 --- a/drakrun/web/storage.py +++ b/drakrun/web/storage.py @@ -232,6 +232,31 @@ def open_seekable_stream( file.close() +def list_analysis_files( + analysis_id: str, s3_config: S3StorageConfigSection +) -> list[str]: + if not is_s3_enabled(s3_config): + base_path = ANALYSES_DIR / analysis_id + if not base_path.exists(): + raise FileNotFoundError + return [ + path.relative_to(base_path).as_posix() + for path in base_path.rglob("*") + if path.is_file() + ] + # S3 handling + s3_client = get_s3_client(s3_config) + analysis_key = get_s3_prefix(analysis_id) + "/" + response = s3_client.list_objects_v2(Bucket=s3_config.bucket, Prefix=analysis_key)[ + "Contents" + ] + keys = [] + for obj in response: + object_name = obj["Key"][len(analysis_key) :] + keys.append(object_name) + return keys + + def list_analysis_logs(analysis_id: str, s3_config: S3StorageConfigSection): if not is_s3_enabled(s3_config): base_path = ANALYSES_DIR / analysis_id