mindee
Version:
Mindee Client Library for Node.js
96 lines (95 loc) • 4.28 kB
JavaScript
import { InputSource } from "./inputSource.js";
import { URL } from "url";
import { basename, extname } from "path";
import { randomBytes } from "crypto";
import { writeFile } from "fs/promises";
import { request, getGlobalDispatcher } from "undici";
import { logger } from "../logger.js";
import { MindeeInputSourceError } from "../errors/index.js";
import { BytesInput } from "./bytesInput.js";
export class UrlInput extends InputSource {
constructor({ url, dispatcher }) {
super();
this.url = url;
this.dispatcher = dispatcher ?? getGlobalDispatcher();
logger.debug("Initialized URL input source.");
}
async init() {
if (this.initialized) {
return;
}
logger.debug(`source URL: ${this.url}`);
if (!this.url.toLowerCase().startsWith("https")) {
throw new MindeeInputSourceError("URL must be HTTPS");
}
this.fileObject = this.url;
this.initialized = true;
}
async fetchFileContent(options) {
const { username, password, token, headers = {}, maxRedirects = 3 } = options;
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const auth = username && password ? `${username}:${password}` : undefined;
return await this.makeRequest(this.url, auth, headers, 0, maxRedirects);
}
async saveToFile(options) {
const { filepath, filename, ...fetchOptions } = options;
const { content, finalUrl } = await this.fetchFileContent(fetchOptions);
const finalFilename = this.fillFilename(filename, finalUrl);
const fullPath = `${filepath}/${finalFilename}`;
await writeFile(fullPath, content);
return fullPath;
}
async asLocalInputSource(options = {}) {
const { filename, ...fetchOptions } = options;
const { content, finalUrl } = await this.fetchFileContent(fetchOptions);
const finalFilename = this.fillFilename(filename, finalUrl);
return new BytesInput({ inputBytes: content, filename: finalFilename });
}
static extractFilenameFromUrl(uri) {
return basename(new URL(uri).pathname || "");
}
static generateFileName(extension = ".tmp") {
const randomString = randomBytes(4).toString("hex");
const timestamp = new Date().toISOString().replace(/[-:]/g, "").split(".")[0];
return `mindee_temp_${timestamp}_${randomString}${extension}`;
}
static getFileExtension(filename) {
const ext = extname(filename);
return ext ? ext.toLowerCase() : null;
}
fillFilename(filename, finalUrl) {
if (!filename) {
filename = finalUrl ? UrlInput.extractFilenameFromUrl(finalUrl) : UrlInput.extractFilenameFromUrl(this.url);
}
if (!filename || !extname(filename)) {
filename = UrlInput.generateFileName(UrlInput.getFileExtension(filename || "") || undefined);
}
return filename;
}
async makeRequest(url, auth, headers, redirects, maxRedirects) {
const parsedUrl = new URL(url);
const response = await request(parsedUrl, {
method: "GET",
headers: headers,
throwOnError: false,
dispatcher: this.dispatcher,
});
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400) {
logger.debug(`Redirecting to: ${response.headers.location}`);
if (redirects === maxRedirects) {
throw new MindeeInputSourceError(`Can't reach URL after ${redirects} out of ${maxRedirects} redirects, aborting operation.`);
}
if (response.headers.location) {
return await this.makeRequest(response.headers.location.toString(), auth, headers, redirects + 1, maxRedirects);
}
throw new MindeeInputSourceError("Redirect location not found");
}
if (!response.statusCode || response.statusCode >= 400 || response.statusCode < 200) {
throw new Error(`Couldn't retrieve file from server, error code ${response.statusCode}.`);
}
const arrayBuffer = await response.body.arrayBuffer();
return { content: Buffer.from(arrayBuffer), finalUrl: url };
}
}