* Protected API route to fetch a single file by ID
*
* @route GET /api/files/:fileId
* @authentication Required
* @param {string} fileId - File ID from the URL parameter
* @returns {Object} File object with all metadata
* @error 400 - If fileId is invalid format
* @error 404 - If file is not found
* @error 403 - If user lacks permission to access the file
* @description Fetches a single file with all its metadata, similar to the files list endpoint
* but for a specific file ID. Used for file detail pages.
*/
files.get("/:fileId", authenticate, async (c) => {
try {
const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;
const userId = jwtPayload.sub;
const fileId = c.req.param("fileId");
if (!fileId) {
return c.json({ error: "fileId is required" }, 400);
}
// Validate fileId format
if (!isValidFileId(fileId)) {
return c.json({ error: "Invalid fileId format" }, 400);
}
const db = createDatabase(c.env);
// Fetch the file with all related data
const fileResult = await db
.select({
id: file.id,
fileName: file.fileName,
path: file.path,
timestampLocal: file.timestampLocal,
duration: file.duration,
sampleRate: file.sampleRate,
locationId: file.locationId,
description: file.description,
upload: file.upload,
clusterId: file.clusterId,
maybeSolarNight: file.maybeSolarNight,
maybeCivilNight: file.maybeCivilNight,
moonPhase: file.moonPhase,
// Include dataset ID for permission checking
datasetId: dataset.id,
datasetOwner: dataset.owner,
// Metadata
metadata: fileMetadata.json,
// Moth metadata
mothGain: mothMetadata.gain,
mothBatteryV: mothMetadata.batteryV,
mothTempC: mothMetadata.tempC,
})
.from(file)
.leftJoin(fileMetadata, eq(fileMetadata.fileId, file.id))
.leftJoin(mothMetadata, eq(mothMetadata.fileId, file.id))
.innerJoin(cluster, eq(cluster.id, file.clusterId))
.innerJoin(location, eq(location.id, cluster.locationId))
.innerJoin(dataset, eq(dataset.id, location.datasetId))
.where(and(
eq(file.id, fileId),
eq(file.active, true)
))
.limit(1);
if (fileResult.length === 0) {
return c.json({ error: "File not found" }, 404);
}
const fileData = fileResult[0];
// Check permissions - user must have READ access to the dataset
const hasPermission = await checkUserPermission(db, userId, fileData.datasetId, 'READ');
if (!hasPermission) {
return c.json({ error: "Insufficient permissions to access this file" }, 403);
}
// Format the response to match the structure expected by the frontend
const formattedFile = {
id: fileData.id,
fileName: fileData.fileName,
path: fileData.path,
timestampLocal: fileData.timestampLocal,
duration: Number(fileData.duration), // Convert from Decimal to number
sampleRate: fileData.sampleRate,
locationId: fileData.locationId,
clusterId: fileData.clusterId,
description: fileData.description,
upload: fileData.upload,
maybeSolarNight: fileData.maybeSolarNight,
maybeCivilNight: fileData.maybeCivilNight,
moonPhase: fileData.moonPhase ? Number(fileData.moonPhase) : null, // Convert from Decimal to number
metadata: fileData.metadata,
mothMetadata: {
gain: fileData.mothGain,
batteryV: fileData.mothBatteryV ? Number(fileData.mothBatteryV) : null,
tempC: fileData.mothTempC ? Number(fileData.mothTempC) : null,
},
species: [], // Will be populated separately if needed
};
return c.json(formattedFile);
} catch (error) {
return c.json(standardErrorResponse(error, "fetching file"), 500);
}
});
/**