-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathurlInput.ts
More file actions
153 lines (135 loc) · 4.88 KB
/
urlInput.ts
File metadata and controls
153 lines (135 loc) · 4.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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, Dispatcher, getGlobalDispatcher } from "undici";
import { logger } from "@/logger.js";
import { MindeeInputSourceError } from "@/errors/index.js";
import { BytesInput } from "./bytesInput.js";
export class UrlInput extends InputSource {
public readonly url: string;
public readonly dispatcher;
constructor(
{ url, dispatcher }: { url: string, dispatcher?: 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;
}
private async fetchFileContent(options: {
username?: string;
password?: string;
token?: string;
headers?: Record<string, string>;
maxRedirects?: number;
}): Promise<{ content: Buffer; finalUrl: string }> {
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: {
filepath: string;
filename?: string;
username?: string;
password?: string;
token?: string;
headers?: Record<string, string>;
maxRedirects?: number;
}): Promise<string> {
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: {
filename?: string;
username?: string;
password?: string;
token?: string;
headers?: Record<string, string>;
maxRedirects?: number;
} = {}): Promise<BytesInput> {
const { filename, ...fetchOptions } = options;
const { content, finalUrl } = await this.fetchFileContent(fetchOptions);
const finalFilename = this.fillFilename(filename, finalUrl);
return new BytesInput({ inputBytes: content, filename: finalFilename });
}
private static extractFilenameFromUrl(uri: string): string {
return basename(new URL(uri).pathname || "");
}
private static generateFileName(extension = ".tmp"): string {
const randomString = randomBytes(4).toString("hex");
const timestamp = new Date().toISOString().replace(/[-:]/g, "").split(".")[0];
return `mindee_temp_${timestamp}_${randomString}${extension}`;
}
private static getFileExtension(filename: string): string | null {
const ext = extname(filename);
return ext ? ext.toLowerCase() : null;
}
private fillFilename(filename?: string, finalUrl?: string): string {
if (!filename) {
filename = finalUrl ? UrlInput.extractFilenameFromUrl(finalUrl) : UrlInput.extractFilenameFromUrl(this.url);
}
if (!filename || !extname(filename)) {
filename = UrlInput.generateFileName(
UrlInput.getFileExtension(filename || "") || undefined
);
}
return filename;
}
private async makeRequest(
url: string,
auth: string | undefined,
headers: Record<string, string>,
redirects: number,
maxRedirects: number
): Promise<{ content: Buffer; finalUrl: string }> {
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 };
}
}