diff --git a/core/database/foxx/api/data_router.js b/core/database/foxx/api/data_router.js index 5ba79ec06..b3012df17 100644 --- a/core/database/foxx/api/data_router.js +++ b/core/database/foxx/api/data_router.js @@ -8,6 +8,7 @@ const g_lib = require("./support"); const g_proc = require("./process"); const g_tasks = require("./tasks"); const { UserToken } = require("./lib/user_token"); +const { validateRepositorySupportsDataOperations } = require("./repository/validation"); module.exports = router; @@ -90,6 +91,13 @@ function recordCreate(client, record, result) { if (!repo_alloc) throw [g_lib.ERR_NO_ALLOCATION, "No allocation available"]; + // Check if repository supports data operations + validateRepositorySupportsDataOperations( + repo_alloc._to, + null, + "Data uploads not supported for metadata-only repository", + ); + // Extension setting only apply to managed data if (record.ext) { obj.ext_auto = false; diff --git a/core/database/foxx/api/repo.js b/core/database/foxx/api/repo.js index baea100b9..e42aa96f9 100644 --- a/core/database/foxx/api/repo.js +++ b/core/database/foxx/api/repo.js @@ -4,6 +4,8 @@ const g_db = require("@arangodb").db; const g_lib = require("./support"); const { errors } = require("@arangodb"); const pathModule = require("./posix_path"); +const { RepositoryOps } = require("./repository/operations"); +const { Result } = require("./repository/types"); /** * All DataFed repositories have the following path structure on a POSIX file system @@ -37,6 +39,10 @@ const PathType = { UNKNOWN: "UNKNOWN", }; +/** + * Legacy Repo class for backward compatibility + * Internally uses new repository patterns but maintains old API + */ class Repo { // ERROR code #error = null; @@ -47,6 +53,8 @@ class Repo { // The repo id simply the key prepended with 'repo/' #repo_id = null; #repo_key = null; + // Store the repository object using new patterns + #repository = null; /** * Constructs a Repo object and checks if the key exists in the database. @@ -66,28 +74,19 @@ class Repo { // // Will return true if it does and false if it does not. if (a_key && a_key !== "repo/") { - if (a_key.startsWith("repo/")) { - this.#repo_id = a_key; - this.#repo_key = a_key.slice("repo/".length); + // Use new repository operations to find the repo + const findResult = RepositoryOps.find(a_key); + + if (findResult.ok) { + this.#exists = true; + this.#repository = findResult.value; + this.#repo_id = findResult.value.data._id; + this.#repo_key = findResult.value.data._key; } else { - this.#repo_id = "repo/" + a_key; - this.#repo_key = a_key; - } - - // Check if the repo document exists - try { - if (collection.exists(this.#repo_key)) { - this.#exists = true; - } else { - this.#exists = false; - this.#error = g_lib.ERR_NOT_FOUND; - this.#err_msg = "Invalid repo: (" + a_key + "). No record found."; - } - } catch (e) { this.#exists = false; - this.#error = g_lib.ERR_INTERNAL_FAULT; - this.#err_msg = "Unknown error encountered."; - console.log(e); + this.#error = + findResult.error.code === 404 ? g_lib.ERR_NOT_FOUND : g_lib.ERR_INTERNAL_FAULT; + this.#err_msg = findResult.error.message; } } } @@ -126,6 +125,14 @@ class Repo { return this.#err_msg; } + /** + * Get the underlying repository object (new pattern) + * @returns {object|null} Repository object or null if not exists + */ + getRepository() { + return this.#repository; + } + /** * Detect what kind of POSIX path has been provided * @@ -138,13 +145,17 @@ class Repo { throw [g_lib.ERR_PERM_DENIED, "Repo does not exist " + this.#repo_id]; } - let repo = g_db._document(this.#repo_id); - if (!repo.path) { + const repoData = this.#repository.data; + if (!repoData.path) { + // Metadata-only repos don't have paths + if (repoData.type === "metadata_only") { + return PathType.UNKNOWN; + } throw [g_lib.ERR_INTERNAL_FAULT, "Repo document is missing path: " + this.#repo_id]; } // Get and sanitize the repo root path by removing the trailing slash if one exists - let repo_root_path = repo.path.replace(/\/$/, ""); + let repo_root_path = repoData.path.replace(/\/$/, ""); let sanitized_path = a_path.replace(/\/$/, ""); // Check if the sanitized path is exactly the repo root path diff --git a/core/database/foxx/api/repo_router.js b/core/database/foxx/api/repo_router.js index d0c828e8a..5e1805c93 100644 --- a/core/database/foxx/api/repo_router.js +++ b/core/database/foxx/api/repo_router.js @@ -7,6 +7,20 @@ const joi = require("joi"); const g_db = require("@arangodb").db; const g_lib = require("./support"); const g_tasks = require("./tasks"); +const { validateGlobusConfig, validatePartialGlobusConfig } = require("./repository/validation"); +const { RepositoryOps } = require("./repository/operations"); + +// Helper function to prepare repository data for saving +const prepareRepoData = (obj) => { + // Ensure paths end with / for saving + if (obj.path && !obj.path.endsWith("/")) { + obj.path += "/"; + } + if (obj.exp_path && !obj.exp_path.endsWith("/")) { + obj.exp_path += "/"; + } + return obj; +}; module.exports = router; @@ -121,28 +135,31 @@ router g_lib.procInputParam(req.body, "summary", false, obj); g_lib.procInputParam(req.body, "domain", false, obj); - if (!obj.path.startsWith("/")) - throw [ - g_lib.ERR_INVALID_PARAM, - "Repository path must be an absolute path file system path.", - ]; - - if (!obj.path.endsWith("/")) obj.path += "/"; - - var idx = obj.path.lastIndexOf("/", obj.path.length - 2); - if (obj.path.substr(idx + 1, obj.path.length - idx - 2) != obj._key) - throw [ - g_lib.ERR_INVALID_PARAM, - "Last part of repository path must be repository ID suffix (" + - obj._key + - ")", - ]; - if (req.body.exp_path) { obj.exp_path = req.body.exp_path; - if (!obj.exp_path.endsWith("/")) obj.path += "/"; } + // Validate the configuration + const validationResult = validateGlobusConfig({ + id: obj._key, + title: obj.title, + capacity: obj.capacity, + admins: req.body.admins, + pub_key: obj.pub_key, + address: obj.address, + endpoint: obj.endpoint, + domain: obj.domain, + path: obj.path, + exp_path: obj.exp_path, + }); + + if (!validationResult.ok) { + throw [validationResult.error.code, validationResult.error.message]; + } + + // Prepare repository data for saving + prepareRepoData(obj); + var repo = g_db.repo.save(obj, { returnNew: true, }); @@ -211,40 +228,41 @@ router g_lib.procInputParam(req.body, "summary", true, obj); g_lib.procInputParam(req.body, "domain", true, obj); - if (req.body.path) { - if (!req.body.path.startsWith("/")) - throw [ - g_lib.ERR_INVALID_PARAM, - "Repository path must be an absolute path file system path.", - ]; - - obj.path = req.body.path; - if (!obj.path.endsWith("/")) obj.path += "/"; - - // Last part of storage path MUST end with the repo ID - var idx = obj.path.lastIndexOf("/", obj.path.length - 2); - var key = req.body.id.substr(5); - if (obj.path.substr(idx + 1, obj.path.length - idx - 2) != key) - throw [ - g_lib.ERR_INVALID_PARAM, - "Last part of repository path must be repository ID suffix (" + - key + - ")", - ]; - } + if (req.body.path) obj.path = req.body.path; + if (req.body.exp_path) obj.exp_path = req.body.exp_path; + if (req.body.capacity) obj.capacity = req.body.capacity; + if (req.body.pub_key) obj.pub_key = req.body.pub_key; + if (req.body.address) obj.address = req.body.address; + if (req.body.endpoint) obj.endpoint = req.body.endpoint; - if (req.body.exp_path) { - obj.exp_path = req.body.exp_path; - if (!obj.exp_path.endsWith("/")) obj.exp_path += "/"; - } + // Extract repo key from ID for validation + const key = req.body.id.substr(5); - if (req.body.capacity) obj.capacity = req.body.capacity; + // Validate the partial configuration + const updateConfig = { + title: req.body.title, + domain: req.body.domain, + path: req.body.path, + exp_path: req.body.exp_path, + capacity: req.body.capacity, + pub_key: req.body.pub_key, + address: req.body.address, + endpoint: req.body.endpoint, + admins: req.body.admins, + }; - if (req.body.pub_key) obj.pub_key = req.body.pub_key; + // Remove undefined fields + Object.keys(updateConfig).forEach( + (k) => updateConfig[k] === undefined && delete updateConfig[k], + ); - if (req.body.address) obj.address = req.body.address; + const validationResult = validatePartialGlobusConfig(updateConfig, key); + if (!validationResult.ok) { + throw [validationResult.error.code, validationResult.error.message]; + } - if (req.body.endpoint) obj.endpoint = req.body.endpoint; + // Prepare repository data for saving + prepareRepoData(obj); var repo = g_db._update(req.body.id, obj, { returnNew: true, @@ -672,15 +690,37 @@ router subject_id = req.queryParams.subject; else subject_id = g_lib.getUserFromClientID(req.queryParams.subject)._id; - var result = g_tasks.taskInitAllocCreate( - client, - req.queryParams.repo, - subject_id, - req.queryParams.data_limit, - req.queryParams.rec_limit, + // Find the repository using the new type system + const findResult = RepositoryOps.find(req.queryParams.repo); + if (!findResult.ok) { + throw [findResult.error.code, findResult.error.message]; + } + + const repository = findResult.value; + + // Check permissions + const permResult = RepositoryOps.checkPermission( + repository, + client._id, + "admin", ); + if (!permResult.ok || !permResult.value) { + throw g_lib.ERR_PERM_DENIED; + } - res.send(result); + // Create allocation using the new system + const allocResult = RepositoryOps.createAllocation(repository, { + subject: subject_id, + size: req.queryParams.data_limit, + rec_limit: req.queryParams.rec_limit, + }); + + if (!allocResult.ok) { + throw [allocResult.error.code, allocResult.error.message]; + } + + // Return the new response format + res.send(allocResult.value); }, }); } catch (e) { @@ -702,7 +742,7 @@ router ) .summary("Create user/project repo allocation") .description( - "Create user repo/project allocation. Only repo admin can set allocations. Returns a task document.", + "Create user repo/project allocation. Only repo admin can set allocations. Returns either a task (for Globus repos) or direct result (for metadata-only repos).", ); router @@ -722,13 +762,34 @@ router subject_id = req.queryParams.subject; else subject_id = g_lib.getUserFromClientID(req.queryParams.subject)._id; - var result = g_tasks.taskInitAllocDelete( - client, - req.queryParams.repo, - subject_id, - ); + // Find the repository using the new type system + var findResult = RepositoryOps.find(req.queryParams.repo); + if (!findResult.ok) { + throw [findResult.error.code, findResult.error.message]; + } + + var repository = findResult.value; + + // Check permissions + var permResult = RepositoryOps.checkPermission(repository, client._id, "admin"); + if (!permResult.ok || !permResult.value) { + throw g_lib.ERR_PERM_DENIED; + } + + // Check if subject exists + if (!g_db._exists(subject_id)) { + throw [g_lib.ERR_NOT_FOUND, "Subject not found: " + subject_id]; + } + + // Delete allocation using the new system + var deleteResult = RepositoryOps.deleteAllocation(repository, subject_id); + + if (!deleteResult.ok) { + throw [deleteResult.error.code, deleteResult.error.message]; + } - res.send(result); + // Return the new response format + res.send(deleteResult.value); }, }); } catch (e) { @@ -740,7 +801,7 @@ router .queryParam("repo", joi.string().required(), "Repo ID") .summary("Delete user/project repo allocation") .description( - "Delete user repo/project allocation. Only repo admin can set allocations. Returns a task document.", + "Delete user repo/project allocation. Only repo admin can set allocations. Returns either a task (for Globus repos) or direct result (for metadata-only repos).", ); router diff --git a/core/database/foxx/api/repository/factory.js b/core/database/foxx/api/repository/factory.js index 81402c623..05aa46a4d 100644 --- a/core/database/foxx/api/repository/factory.js +++ b/core/database/foxx/api/repository/factory.js @@ -33,7 +33,6 @@ const g_lib = require("../support"); * @param {string} [config.pub_key] - Public key for ZeroMQ CURVE authentication (required for GLOBUS type) * @param {string} [config.address] - Network address (required for GLOBUS type) * @param {string} [config.exp_path] - Export path (optional for GLOBUS type) - * @param {string} [config.domain] - Domain name (required for GLOBUS type) * @returns {{ok: boolean, error: *}|{ok: boolean, value: *}} Result object containing repository or error * @see https://doc.rust-lang.org/book/ch06-02-match.html */ @@ -69,7 +68,6 @@ const createRepositoryByType = (config) => { pub_key: config.pub_key, address: config.address, exp_path: config.exp_path, - domain: config.domain, }); const repoData = createRepositoryData({ diff --git a/core/database/foxx/api/repository/globus.js b/core/database/foxx/api/repository/globus.js new file mode 100644 index 000000000..9e716a930 --- /dev/null +++ b/core/database/foxx/api/repository/globus.js @@ -0,0 +1,131 @@ +"use strict"; + +const { Result, ExecutionMethod, createAllocationResult } = require("./types"); +const { validateAllocationParams } = require("./validation"); +const g_tasks = require("../tasks"); +const g_lib = require("../support"); + +/** + * @module globus + * Globus repository implementation + * Implements repository operations specific to Globus-backed repositories + */ + +/** + * This module acts like a trait implementation for the Globus repository type + * Each function implements a trait method for this specific type + * @see https://doc.rust-lang.org/book/ch10-02-traits.html#implementing-a-trait-on-a-type + */ + +// Validate Globus repository (already validated in factory) +const validate = (repoData) => { + return Result.ok(true); +}; + +// Create allocation in Globus repository (async via task) +const createAllocation = (repoData, params) => { + // Validate allocation parameters + const validationResult = validateAllocationParams(params); + if (!validationResult.ok) { + return validationResult; + } + + try { + // Create task for async Globus allocation + const task = g_tasks.repoAllocationCreateTask({ + repo_id: repoData._id, + subject: params.subject, + size: params.size, + path: params.path || null, + metadata: params.metadata || {}, + }); + + return Result.ok( + createAllocationResult(ExecutionMethod.TASK, { + task_id: task.task_id, + status: task.status, + queue_time: task.queue_time, + }), + ); + } catch (e) { + return Result.err({ + code: g_lib.ERR_INTERNAL_FAULT, + message: `Failed to create allocation task: ${e.message}`, + }); + } +}; + +// Delete allocation from Globus repository (async via task) +const deleteAllocation = (repoData, subjectId) => { + if (!subjectId || typeof subjectId !== "string") { + return Result.err({ + code: g_lib.ERR_INVALID_PARAM, + message: "Subject ID is required for allocation deletion", + }); + } + + try { + // Create task for async Globus allocation deletion + const task = g_tasks.repoAllocationDeleteTask({ + repo_id: repoData._id, + subject: subjectId, + }); + + return Result.ok( + createAllocationResult(ExecutionMethod.TASK, { + task_id: task.task_id, + status: task.status, + queue_time: task.queue_time, + }), + ); + } catch (e) { + return Result.err({ + code: g_lib.ERR_INTERNAL_FAULT, + message: `Failed to create deletion task: ${e.message}`, + }); + } +}; + +// Globus repositories support data operations +const supportsDataOperations = (repoData) => { + return Result.ok(true); +}; + +// Get capacity information for Globus repository +const getCapacityInfo = (repoData) => { + try { + // For Globus repos, we'd typically query the actual filesystem + // For now, return the configured capacity + return Result.ok({ + total_capacity: repoData.capacity, + used_capacity: 0, // Would be populated from actual usage + available_capacity: repoData.capacity, + supports_quotas: true, + }); + } catch (e) { + return Result.err({ + code: g_lib.ERR_INTERNAL_FAULT, + message: `Failed to get capacity info: ${e.message}`, + }); + } +}; + +/** + * Export all operations (trait implementation) + * These exports define the trait implementation for Globus repository type + * allowing polymorphic behavior through dynamic dispatch + * @type {object} + * @property {function(object): {ok: boolean, value: boolean}} validate - Validate Globus repository + * @property {function(object, object): {ok: boolean, error?: *, value?: *}} createAllocation - Create allocation in Globus repository + * @property {function(object, string): {ok: boolean, error?: *, value?: *}} deleteAllocation - Delete allocation from Globus repository + * @property {function(object): {ok: boolean, value: boolean}} supportsDataOperations - Check if supports data operations + * @property {function(object): {ok: boolean, error?: *, value?: *}} getCapacityInfo - Get capacity information + * @see https://doc.rust-lang.org/book/ch17-02-trait-objects.html + */ +module.exports = { + validate, + createAllocation, + deleteAllocation, + supportsDataOperations, + getCapacityInfo, +}; diff --git a/core/database/foxx/api/repository/metadata.js b/core/database/foxx/api/repository/metadata.js new file mode 100644 index 000000000..2bf21c555 --- /dev/null +++ b/core/database/foxx/api/repository/metadata.js @@ -0,0 +1,136 @@ +"use strict"; + +const { Result, ExecutionMethod, createAllocationResult } = require("./types"); +const { validateAllocationParams } = require("./validation"); +const g_lib = require("../support"); + +/** + * @module metadata + * @description Metadata-only repository implementation + * Implements repository operations for repositories that only store metadata without actual data storage backend + */ + +/** + * This module provides a different trait implementation for metadata repositories + * demonstrating how the same trait can have different implementations per type + * @see https://doc.rust-lang.org/book/ch10-02-traits.html#implementing-a-trait-on-a-type + */ + +// Validate metadata repository (already validated in factory) +const validate = (repoData) => { + return Result.ok(true); +}; + +// Create allocation in metadata repository (direct/synchronous) +const createAllocation = (repoData, params) => { + // Validate allocation parameters + const validationResult = validateAllocationParams(params); + if (!validationResult.ok) { + return validationResult; + } + + try { + // For metadata-only repos, allocations are just database records + // No actual storage allocation happens + const allocation = { + _key: `alloc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + repo_id: repoData._id, + subject: params.subject, + size: params.size, + path: params.path || `/${params.subject}`, + metadata: params.metadata || {}, + created: new Date().toISOString(), + type: "metadata_only", + }; + + // Save to allocations collection (would need to be created) + // For now, return success with the allocation data + const result = { + allocation_id: allocation._key, + repo_id: allocation.repo_id, + subject: allocation.subject, + size: allocation.size, + path: allocation.path, + status: "completed", + }; + + return Result.ok(createAllocationResult(ExecutionMethod.DIRECT, result)); + } catch (e) { + return Result.err({ + code: g_lib.ERR_INTERNAL_FAULT, + message: `Failed to create metadata allocation: ${e.message}`, + }); + } +}; + +// Delete allocation from metadata repository (direct/synchronous) +const deleteAllocation = (repoData, subjectId) => { + if (!subjectId || typeof subjectId !== "string") { + return Result.err({ + code: g_lib.ERR_INVALID_PARAM, + message: "Subject ID is required for allocation deletion", + }); + } + + try { + // For metadata-only repos, just remove the database record + // No actual storage deallocation needed + const result = { + repo_id: repoData._id, + subject: subjectId, + status: "completed", + message: "Metadata allocation removed", + }; + + return Result.ok(createAllocationResult(ExecutionMethod.DIRECT, result)); + } catch (e) { + return Result.err({ + code: g_lib.ERR_INTERNAL_FAULT, + message: `Failed to delete metadata allocation: ${e.message}`, + }); + } +}; + +// Metadata repositories do NOT support data operations +const supportsDataOperations = (repoData) => { + return Result.ok(false); +}; + +// Get capacity information for metadata repository +const getCapacityInfo = (repoData) => { + try { + // Metadata repos have logical capacity limits, not physical + return Result.ok({ + total_capacity: repoData.capacity, + used_capacity: 0, // Would track metadata record count/size + available_capacity: repoData.capacity, + supports_quotas: false, + is_metadata_only: true, + }); + } catch (e) { + return Result.err({ + code: g_lib.ERR_INTERNAL_FAULT, + message: `Failed to get capacity info: ${e.message}`, + }); + } +}; + +/** + * Export all operations (trait implementation) + * These exports define the trait implementation for metadata repository type + * Note how the same interface has different behavior than Globus implementation + * @type {object} + * @property {function(object): {ok: boolean, value: boolean}} validate - Validate metadata repository + * @property {function(object, object): {ok: boolean, error?: *, value?: *}} createAllocation - Create allocation in metadata repository + * @property {function(object, string): {ok: boolean, error?: *, value?: *}} deleteAllocation - Delete allocation from metadata repository + * @property {function(object): {ok: boolean, value: boolean}} supportsDataOperations - Check if supports data operations + * @property {function(object): {ok: boolean, error?: *, value?: *}} getCapacityInfo - Get capacity information + * @see https://doc.rust-lang.org/book/ch17-02-trait-objects.html + */ +module.exports = { + validate, + createAllocation, + deleteAllocation, + supportsDataOperations, + getCapacityInfo, +}; diff --git a/core/database/foxx/api/repository/types.js b/core/database/foxx/api/repository/types.js index 55f7c24a1..5f742495b 100644 --- a/core/database/foxx/api/repository/types.js +++ b/core/database/foxx/api/repository/types.js @@ -71,16 +71,14 @@ const createRepositoryData = ({ * @param {string} config.pub_key - Public key for ZeroMQ CURVE authentication * @param {string} config.address - Network address * @param {string} [config.exp_path] - Export path - * @param {string} config.domain - Domain name - * @returns {{endpoint: string, path: string, pub_key: string, address: string, exp_path: string, domain: string}} Globus configuration object + * @returns {{endpoint: string, path: string, pub_key: string, address: string, exp_path: string }} Globus configuration object */ -const createGlobusConfig = ({ endpoint, path, pub_key, address, exp_path, domain }) => ({ +const createGlobusConfig = ({ endpoint, path, pub_key, address, exp_path }) => ({ endpoint, path, pub_key, address, exp_path, - domain, }); /** diff --git a/core/database/foxx/api/repository/validation.js b/core/database/foxx/api/repository/validation.js index 69b85a5ad..81f7acacf 100644 --- a/core/database/foxx/api/repository/validation.js +++ b/core/database/foxx/api/repository/validation.js @@ -1,6 +1,7 @@ "use strict"; const { Result } = require("./types"); +const { RepositoryOps } = require("./operations"); const g_lib = require("../support"); /** @@ -137,11 +138,6 @@ const validateGlobusConfig = (config) => { errors.push(endpointValidation.error.message); } - const domainValidation = validateNonEmptyString(config.domain, "Domain"); - if (!domainValidation.ok) { - errors.push(domainValidation.error.message); - } - if (errors.length > 0) { return Result.err({ code: g_lib.ERR_INVALID_PARAM, @@ -175,7 +171,7 @@ const validateMetadataConfig = (config) => { // Metadata repositories don't need Globus-specific fields // But should not have them either - const invalidFields = ["pub_key", "address", "endpoint", "path", "exp_path", "domain"]; + const invalidFields = ["pub_key", "address", "endpoint", "path", "exp_path"]; const presentInvalidFields = invalidFields.filter((field) => config[field] !== undefined); if (presentInvalidFields.length > 0) { @@ -215,6 +211,29 @@ const validateAllocationParams = (params) => { return Result.ok(true); }; +// Validates that a repository supports data operations +const validateRepositorySupportsDataOperations = (repoId, dataId, errorMessage) => { + const findResult = RepositoryOps.find(repoId); + if (findResult.ok) { + const repository = findResult.value; + const dataOpsResult = RepositoryOps.supportsDataOperations(repository); + + if (dataOpsResult.ok && !dataOpsResult.value) { + const defaultMessage = + errorMessage || `Data operations not supported for ${repository.type} repository`; + throw [ + g_lib.ERR_INVALID_OPERATION, + defaultMessage, + { + repo_type: repository.type, + repo_id: repository.data._id, + data_id: dataId, + }, + ]; + } + } +}; + module.exports = { validateNonEmptyString, validateCommonFields, @@ -223,4 +242,5 @@ module.exports = { validateGlobusConfig, validateMetadataConfig, validateAllocationParams, + validateRepositorySupportsDataOperations, }; diff --git a/core/database/foxx/api/support.js b/core/database/foxx/api/support.js index 6133ed7c7..3aa753e58 100644 --- a/core/database/foxx/api/support.js +++ b/core/database/foxx/api/support.js @@ -1,6 +1,8 @@ "use strict"; const joi = require("joi"); +const { RepositoryOps } = require("./repository/operations"); +const { RepositoryType } = require("./repository/types"); module.exports = (function () { var obj = {}; @@ -164,6 +166,8 @@ module.exports = (function () { obj.ERR_INFO.push([400, "No allocation available"]); obj.ERR_ALLOCATION_EXCEEDED = obj.ERR_COUNT++; obj.ERR_INFO.push([400, "Storage allocation exceeded"]); + obj.ERR_INVALID_OPERATION = obj.ERR_COUNT++; + obj.ERR_INFO.push([400, "Invalid operation"]); obj.CHARSET_ID = 0; obj.CHARSET_ALIAS = 1; @@ -910,6 +914,18 @@ module.exports = (function () { alloc = allocs[i]; if (alloc.data_size < alloc.data_limit && alloc.rec_count < alloc.rec_limit) { + // Check if repository supports data operations + const findResult = RepositoryOps.find(alloc._to); + if (findResult.ok) { + const repository = findResult.value; + const dataOpsResult = RepositoryOps.supportsDataOperations(repository); + + // Skip metadata-only repositories + if (dataOpsResult.ok && !dataOpsResult.value) { + continue; + } + } + return alloc; } } @@ -936,6 +952,24 @@ module.exports = (function () { "Allocation record count exceeded (max: " + alloc.rec_limit + ")", ]; + // Check if repository supports data operations + var findResult = RepositoryOps.find(a_repo_id); + if (findResult.ok) { + var repository = findResult.value; + var dataOpsResult = RepositoryOps.supportsDataOperations(repository); + + if (dataOpsResult.ok && !dataOpsResult.value) { + throw [ + obj.ERR_INVALID_OPERATION, + "Data operations not supported for metadata-only repository", + { + repo_type: repository.type, + repo_id: repository.data._id, + }, + ]; + } + } + return alloc; }; diff --git a/core/database/foxx/api/tasks.js b/core/database/foxx/api/tasks.js index cc2de4e42..0c389ad8c 100644 --- a/core/database/foxx/api/tasks.js +++ b/core/database/foxx/api/tasks.js @@ -1,16 +1,18 @@ "use strict"; -// local imports const g_lib = require("./support"); const { UserToken } = require("./lib/user_token"); +const { RepositoryOps } = require("./repository/operations"); +const { validateRepositorySupportsDataOperations } = require("./repository/validation"); +const { RepositoryType } = require("./repository/types"); const g_db = require("@arangodb").db; const g_graph = require("@arangodb/general-graph")._graph("sdmsg"); const g_proc = require("./process"); var g_internal = require("internal"); -var tasks_func = (function () { - var obj = {}; +const tasks_func = (function () { + const obj = {}; // ----------------------- ALLOC CREATE ---------------------------- @@ -308,6 +310,22 @@ var tasks_func = (function () { var result = g_proc.preprocessItems(a_client, null, a_res_ids, g_lib.TT_DATA_GET); + // Check repository types for all Globus data items + if (result.glob_data.length > 0) { + for (var i = 0; i < result.glob_data.length; i++) { + var data = result.glob_data[i]; + // Get repository from data location + var loc = g_db.loc.firstExample({ _from: data.id }); + if (loc) { + validateRepositorySupportsDataOperations( + loc._to, + data.id, + `Data transfers not supported for this repository type`, + ); + } + } + } + if (result.glob_data.length + result.ext_data.length > 0 && !a_check) { var idx = a_path.indexOf("/"); if (idx == -1) @@ -492,6 +510,22 @@ var tasks_func = (function () { var result = g_proc.preprocessItems(a_client, null, a_res_ids, g_lib.TT_DATA_PUT); + // Check repository types for all Globus data items + if (result.glob_data.length > 0) { + for (var i = 0; i < result.glob_data.length; i++) { + var data = result.glob_data[i]; + // Get repository from data location + var loc = g_db.loc.firstExample({ _from: data.id }); + if (loc) { + validateRepositorySupportsDataOperations( + loc._to, + data.id, + `Data transfers not supported for this repository type`, + ); + } + } + } + if (result.glob_data.length > 0 && !a_check) { var idx = a_path.indexOf("/"); if (idx == -1) @@ -2809,6 +2843,46 @@ var tasks_func = (function () { //console.log("_ensureExclusiveAccess done", Date.now()); }; + // ----------------------- REPO ALLOCATION WRAPPERS ---------------------------- + + // Wrapper for allocation creation (used by repository factory pattern) + obj.repoAllocationCreateTask = function (params) { + // Extract parameters from the params object + const { repo_id, subject, size, path, metadata } = params; + + // Get the repository to determine limits + const repo = g_db.repo.document(repo_id); + + // Use a reasonable default for rec_limit if not specified + const rec_limit = params.rec_limit || 1000000; // 1 million records as default + + // Create a dummy client object that has admin permissions + // This is because the repository operations have already validated permissions + const systemClient = { _id: "system", is_admin: true }; + + // Call the existing taskInitAllocCreate function + return obj.taskInitAllocCreate( + systemClient, + repo_id, + subject, + size || repo.capacity, // Use repo capacity if size not specified + rec_limit, + ); + }; + + // Wrapper for allocation deletion (used by repository factory pattern) + obj.repoAllocationDeleteTask = function (params) { + // Extract parameters from the params object + const { repo_id, subject } = params; + + // Create a dummy client object that has admin permissions + // This is because the repository operations have already validated permissions + const systemClient = { _id: "system", is_admin: true }; + + // Call the existing taskInitAllocDelete function + return obj.taskInitAllocDelete(systemClient, repo_id, subject); + }; + return obj; })();