@cessnetwork/api
Version:
CESS Chain Interface Implementation in TypeScript
469 lines • 16.3 kB
JavaScript
import fs from "fs";
import path from "path";
import { pipeline } from "stream/promises";
import { Readable } from "stream";
// Gateway and Retriever API endpoints
export const GATEWAY_GETFILE_URL = "/gateway/download";
export const RETRIEVER_QUERYDATA_URL = "/querydata";
export const RETRIEVER_FETCHDATA_URL = "/cache-fetch";
/**
* Download file from gateway with optional save to disk
*/
export async function downloadFile(config, options) {
const baseUrl = config.baseUrl.replace(/\/$/, "");
try {
let url;
if (options.segmentHash) {
url = `${baseUrl}${GATEWAY_GETFILE_URL}/${options.fid}/${options.segmentHash}`;
}
else {
url = `${baseUrl}${GATEWAY_GETFILE_URL}/${options.fid}`;
}
const headers = {};
// Add Range header for partial downloads
if (options.range) {
const { start, end } = options.range;
headers["Range"] = `bytes=${start}-${end || ''}`;
}
// Add encryption headers if provided
if (options.encryption) {
headers["Capsule"] = options.encryption.capsule;
headers["Rkb"] = options.encryption.rkb;
headers["Pkx"] = options.encryption.pkx;
}
const response = await fetch(url, {
method: "GET",
headers,
});
// Check if request was successful
if (!response.ok) {
// Try to parse error response
try {
const errorData = await response.json();
return {
success: false,
status: response.status,
error: `HTTP ${response.status}: ${response.statusText}`,
};
}
catch {
return {
success: false,
status: response.status,
error: `HTTP ${response.status}: ${response.statusText}`,
};
}
}
const contentType = response.headers.get("content-type");
const contentLength = response.headers.get("content-length");
const contentRange = response.headers.get("content-range");
const isPartialContent = response.status === 206;
// Extract filename from Content-Disposition header if available
const contentDisposition = response.headers.get("content-disposition");
let serverFileName;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch) {
serverFileName = filenameMatch[1].replace(/['"]/g, '');
}
}
// If savePath is provided, save to disk
if (options.savePath) {
const saveResult = await saveResponseToFile(response, options, serverFileName);
return {
success: true,
data: {
contentType,
contentLength,
contentRange,
isPartialContent,
filePath: saveResult.filePath,
fileName: saveResult.fileName,
fileSize: saveResult.fileSize,
},
};
}
else {
// Return as ArrayBuffer for in-memory use
const data = await response.arrayBuffer();
return {
success: true,
data: {
content: data,
contentType,
contentLength,
contentRange,
isPartialContent,
fileSize: data.byteLength,
},
};
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
/**
* Save response stream to file
*/
async function saveResponseToFile(response, options, serverFileName) {
if (!options.savePath) {
throw new Error("Save path is required");
}
const { filePath, fileName } = await prepareSavePath(options.savePath, options.fid, response.headers.get("content-type"), options.createDirectories, options.overwrite, serverFileName);
// Create readable stream from response
const readable = Readable.fromWeb(response.body);
const writeStream = fs.createWriteStream(filePath);
// Use pipeline for efficient streaming
await pipeline(readable, writeStream);
// Get file size
const stats = await fs.promises.stat(filePath);
return {
filePath,
fileName,
fileSize: stats.size,
};
}
/**
* Generate filename from fid and content type
*/
function generateFileName(fid, contentType, serverFileName) {
// Use server-provided filename if available
if (serverFileName) {
return serverFileName;
}
const extension = getExtensionFromContentType(contentType);
// Use first 8 characters of fid as filename
const baseName = fid.substring(0, 8);
return `${baseName}${extension}`;
}
/**
* Get file extension from content type
*/
function getExtensionFromContentType(contentType) {
if (!contentType)
return '.bin';
const mimeToExt = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'image/webp': '.webp',
'image/svg+xml': '.svg',
'text/plain': '.txt',
'text/html': '.html',
'text/css': '.css',
'text/javascript': '.js',
'application/pdf': '.pdf',
'application/json': '.json',
'application/xml': '.xml',
'video/mp4': '.mp4',
'video/webm': '.webm',
'video/avi': '.avi',
'video/quicktime': '.mov',
'audio/mpeg': '.mp3',
'audio/wav': '.wav',
'audio/ogg': '.ogg',
'application/zip': '.zip',
'application/x-rar-compressed': '.rar',
'application/x-7z-compressed': '.7z',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/octet-stream': '.bin',
};
return mimeToExt[contentType.toLowerCase()] || '.bin';
}
/**
* Check if file exists
*/
async function fileExists(filePath) {
try {
await fs.promises.access(filePath);
return true;
}
catch {
return false;
}
}
async function prepareSavePath(savePath, fid, contentType, createDirectories, overwrite, serverFileName) {
let filePath = savePath;
let fileName;
const isDirectory = filePath.endsWith('/') || filePath.endsWith('\\') ||
(!path.extname(filePath) && !filePath.includes('.'));
if (isDirectory) {
fileName = generateFileName(fid, contentType, serverFileName);
filePath = path.join(filePath, fileName);
}
else {
fileName = path.basename(filePath);
}
if (createDirectories !== false) {
const directory = path.dirname(filePath);
await fs.promises.mkdir(directory, { recursive: true });
}
if (!overwrite && await fileExists(filePath)) {
throw new Error(`File already exists: ${filePath}. Set overwrite: true to replace it.`);
}
return { filePath, fileName };
}
/**
* Download file with range request support (useful for resuming downloads)
*/
export async function downloadFileRange(config, options, start, end) {
const rangeOptions = {
...options,
range: { start, end },
};
return downloadFile(config, rangeOptions);
}
/**
* Download encrypted file
*/
export async function downloadEncryptedFile(config, options, encryptionData) {
const encryptedOptions = {
...options,
encryption: encryptionData,
};
return downloadFile(config, encryptedOptions);
}
/**
* Fetch file metadata without downloading content
*/
export async function getFileMetadata(config, options) {
const baseUrl = config.baseUrl.replace(/\/$/, "");
try {
// Build URL path according to Gin routing
let url;
if (options.segmentHash) {
url = `${baseUrl}${GATEWAY_GETFILE_URL}/${options.fid}/${options.segmentHash}`;
}
else {
url = `${baseUrl}${GATEWAY_GETFILE_URL}/${options.fid}`;
}
const headers = {};
const response = await fetch(url, {
method: "HEAD",
headers,
});
if (!response.ok) {
return {
success: false,
status: response.status,
error: `HTTP ${response.status}: ${response.statusText}`,
};
}
return {
success: true,
data: {
contentType: response.headers.get("content-type"),
contentLength: response.headers.get("content-length"),
lastModified: response.headers.get("last-modified"),
etag: response.headers.get("etag"),
contentDisposition: response.headers.get("content-disposition"),
},
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
/**
* Download file with progress tracking and support for range requests
*/
export async function downloadFileWithProgress(config, options, onProgress) {
const baseUrl = config.baseUrl.replace(/\/$/, "");
try {
// Build URL path according to Gin routing
let url;
if (options.segmentHash) {
url = `${baseUrl}${GATEWAY_GETFILE_URL}/${options.fid}/${options.segmentHash}`;
}
else {
url = `${baseUrl}${GATEWAY_GETFILE_URL}/${options.fid}`;
}
const headers = {};
// Add Range header if specified
if (options.range) {
const { start, end } = options.range;
headers["Range"] = `bytes=${start}-${end || ''}`;
}
// Add encryption headers if provided
if (options.encryption) {
headers["Capsule"] = options.encryption.capsule;
headers["Rkb"] = options.encryption.rkb;
headers["Pkx"] = options.encryption.pkx;
}
const response = await fetch(url, {
method: "GET",
headers,
});
if (!response.ok) {
try {
const errorData = await response.json();
return {
success: false,
status: response.status,
error: `HTTP ${response.status}: ${response.statusText}`,
};
}
catch {
return {
success: false,
status: response.status,
error: `HTTP ${response.status}: ${response.statusText}`,
};
}
}
const contentType = response.headers.get("content-type");
const contentLength = response.headers.get("content-length");
const contentRange = response.headers.get("content-range");
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
const isPartialContent = response.status === 206;
// Extract filename from Content-Disposition header if available
const contentDisposition = response.headers.get("content-disposition");
let serverFileName;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch) {
serverFileName = filenameMatch[1].replace(/['"]/g, '');
}
}
let downloadedSize = 0;
if (options.savePath) {
// Save to file with progress tracking
const { filePath, fileName } = await prepareSavePath(options.savePath, options.fid, contentType, options.createDirectories, options.overwrite, serverFileName);
const readable = Readable.fromWeb(response.body);
const writeStream = fs.createWriteStream(filePath);
// Track progress
readable.on('data', (chunk) => {
downloadedSize += chunk.length;
if (onProgress && totalSize > 0) {
onProgress(downloadedSize, totalSize);
}
});
await pipeline(readable, writeStream);
const stats = await fs.promises.stat(filePath);
return {
success: true,
data: {
contentType,
contentLength,
contentRange,
isPartialContent,
filePath,
fileName,
fileSize: stats.size,
},
};
}
else {
// Return as ArrayBuffer with progress tracking
const chunks = [];
const reader = response.body?.getReader();
if (!reader) {
throw new Error("Response body is not readable");
}
while (true) {
const { done, value } = await reader.read();
if (done)
break;
chunks.push(value);
downloadedSize += value.length;
if (onProgress && totalSize > 0) {
onProgress(downloadedSize, totalSize);
}
}
const buffer = new Uint8Array(downloadedSize);
let offset = 0;
for (const chunk of chunks) {
buffer.set(chunk, offset);
offset += chunk.length;
}
return {
success: true,
data: {
content: buffer.buffer,
contentType,
contentLength,
contentRange,
isPartialContent,
fileSize: downloadedSize,
},
};
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
// Keep your existing functions for queryData, fetchCacheData, downloadFiles, etc.
// They don't need changes since they don't interact with the download endpoint
/**
* Query data from retriever
*/
export async function queryData(config, options = {}) {
const baseUrl = config.baseUrl.replace(/\/$/, "");
try {
const params = new URLSearchParams();
if (options.hash) {
params.append("hash", options.hash);
}
const url = `${baseUrl}${RETRIEVER_QUERYDATA_URL}?${params.toString()}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Token": config.token,
},
});
const result = await response.json();
return {
success: true,
data: result,
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
/**
* Fetch data from cache
*/
export async function fetchCacheData(config, options) {
const baseUrl = config.baseUrl.replace(/\/$/, "");
try {
const params = new URLSearchParams();
params.append("cacheKey", options.cacheKey);
const url = `${baseUrl}${RETRIEVER_FETCHDATA_URL}?${params.toString()}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Token": config.token,
},
});
const result = await response.json();
return {
success: true,
data: result,
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
// Add batch download functions here if needed...
//# sourceMappingURL=download.js.map