curlie 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ import { Client } from "basic-ftp";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { testWritable } from "./utils.js";
5
+
6
+ export async function ftpList(url, flags = {}) {
7
+ if (!url) throw new Error("No FTP URL provided");
8
+
9
+ const cleanUrl = url.replace(/^ftp:\/\//, "").replace(/\/$/, "");
10
+ const parts = cleanUrl.split("/");
11
+ const hostname = parts.shift();
12
+ const remotePath = parts.join("/") || ".";
13
+
14
+ let [username, password] = (flags.user || "anonymous:anonymous").split(":");
15
+ if (username === "anonymous" && password === "anonymous") {
16
+ password = "anonymous@example.com";
17
+ }
18
+
19
+ const client = new Client();
20
+ client.ftp.verbose = flags.verbose || false;
21
+
22
+ let triedActive = false;
23
+
24
+ const connectAndTransfer = async (useActive = false) => {
25
+ client.ftp.active = !!useActive;
26
+ console.log(useActive ? "Trying ACTIVE mode..." : "Using PASSIVE mode...");
27
+
28
+ try {
29
+ await client.access({
30
+ host: hostname,
31
+ user: username,
32
+ password,
33
+ secure: flags.ssl || flags.sslReq || false,
34
+ });
35
+
36
+ if (flags.quote?.length) {
37
+ for (const cmd of flags.quote) {
38
+ console.log(`Sending QUOTE command: ${cmd}`);
39
+ const res = await client.send(cmd);
40
+ if (res?.message) console.log(res.message);
41
+ }
42
+ }
43
+
44
+ if (flags.method) {
45
+ console.log(`Sending FTP command via -X: ${flags.method}`);
46
+ const res = await client.send(flags.method);
47
+ if (res?.message) console.log(res.message);
48
+ return;
49
+ }
50
+
51
+ if (flags.head) {
52
+ const size = await client.size(remotePath);
53
+ const modified = await client.lastMod(remotePath);
54
+
55
+ console.log("File metadata:");
56
+ console.log("Path:", remotePath);
57
+ console.log("Size:", size, "bytes");
58
+ console.log("Last modified:", modified);
59
+ return;
60
+ }
61
+
62
+ if (flags.l || flags.listOnly) {
63
+ const list = await client.list(remotePath);
64
+ console.log(`Listing files in ${remotePath}:`);
65
+ list.forEach((file) => {
66
+ console.log(`${file.name}\t${file.size}\t${file.date}`);
67
+ });
68
+ return;
69
+ }
70
+
71
+ if (flags.o || flags.output) {
72
+ const localFile = path.resolve(flags.o || flags.output);
73
+ console.log(`Downloading ${remotePath} -> ${localFile}`);
74
+ await client.downloadTo(localFile, remotePath);
75
+ console.log("Download complete.");
76
+ return;
77
+ }
78
+
79
+ if (flags.T || flags.upload) {
80
+ const localFile = flags.T || flags.upload;
81
+
82
+ if (!fs.existsSync(localFile)) {
83
+ throw new Error(`Local file does not exist: ${localFile}`);
84
+ }
85
+
86
+ const writable = await testWritable(client, remotePath);
87
+ if (!writable) {
88
+ console.error(
89
+ `FTP Error: target path "${remotePath}" is not writable.`,
90
+ );
91
+ return;
92
+ }
93
+
94
+ if (flags.a || flags.append) {
95
+ console.log(`Appending ${localFile} -> ${remotePath}`);
96
+ await client.appendFrom(localFile, remotePath);
97
+ } else {
98
+ console.log(`Uploading ${localFile} -> ${remotePath}`);
99
+ await client.uploadFrom(localFile, remotePath);
100
+ }
101
+
102
+ console.log("Upload complete.");
103
+ return;
104
+ }
105
+
106
+ console.log(`Connected to FTP server: ${hostname}`);
107
+ console.log(`Remote path: ${remotePath}`);
108
+ } catch (err) {
109
+ if (!triedActive && !useActive) {
110
+ triedActive = true;
111
+ client.close();
112
+ console.warn("PASSIVE mode failed, falling back to ACTIVE mode...");
113
+ await connectAndTransfer(true);
114
+ } else {
115
+ console.error("FTP Error:", err.message);
116
+ }
117
+ } finally {
118
+ client.close();
119
+ }
120
+ };
121
+
122
+ await connectAndTransfer();
123
+ }
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+ import http from "http";
3
+ import https from "https";
4
+ import fs from "fs";
5
+ import { URL } from "url";
6
+ import { pipeline } from "stream";
7
+ import {
8
+ applyAuth,
9
+ bufferResponse,
10
+ getStreamDestination,
11
+ isBinaryResponse,
12
+ mayWait,
13
+ streamResponse,
14
+ loadCookieJar,
15
+ saveCookieJar,
16
+ } from "./utils.js";
17
+ import { createDnsLookup } from "./dnsHandler.js";
18
+ import { createSpeedMonitorStream } from "./monitor.js";
19
+
20
+ const RETRY_CODES = [408, 429, 500, 502, 503, 504];
21
+
22
+ function verboseLog(flags, message) {
23
+ if (flags.verbose) {
24
+ process.stderr.write(message + "\n");
25
+ }
26
+ }
27
+
28
+ function verboseRequest(flags, options, body) {
29
+ if (!flags.verbose) return;
30
+
31
+ verboseLog(flags, `> ${options.method} ${options.path} HTTP/1.1`);
32
+ for (const [key, value] of Object.entries(options.headers)) {
33
+ verboseLog(flags, `> ${key}: ${value}`);
34
+ }
35
+ if (body) {
36
+ verboseLog(flags, `> `);
37
+ verboseLog(flags, body.substring(0, 1024));
38
+ }
39
+ }
40
+
41
+ function verboseResponse(flags, res, body) {
42
+ if (!flags.verbose) return;
43
+
44
+ verboseLog(flags, `< HTTP/1.1 ${res.statusCode} ${res.statusMessage}`);
45
+ for (const [key, value] of Object.entries(res.headers)) {
46
+ verboseLog(flags, `< ${key}: ${value}`);
47
+ }
48
+ if (body && body.length > 0) {
49
+ verboseLog(flags, `< `);
50
+ verboseLog(flags, body.substring(0, 1024));
51
+ }
52
+ }
53
+
54
+ function sleep(ms) {
55
+ return new Promise((resolve) => setTimeout(resolve, ms));
56
+ }
57
+
58
+ function makeRequest(url, method, flags, redirectCount = 0) {
59
+ return new Promise((resolve, reject) => {
60
+ const parsed = new URL(url);
61
+ const protocol = parsed.protocol === "https:" ? https : http;
62
+
63
+ let headers = applyAuth(flags.headers || {}, flags);
64
+
65
+ if (flags.userAgent) {
66
+ headers["User-Agent"] = flags.userAgent;
67
+ }
68
+ if (flags.referer) {
69
+ headers["Referer"] = flags.referer;
70
+ }
71
+
72
+ let body = null;
73
+
74
+ if (flags.upload) {
75
+ body =
76
+ flags.upload === "-"
77
+ ? fs.readFileSync(0)
78
+ : fs.readFileSync(flags.upload);
79
+ headers["Content-Type"] ||= "application/octet-stream";
80
+ headers["Content-Length"] = Buffer.byteLength(body);
81
+ } else if (flags.json) {
82
+ try {
83
+ if (typeof flags.json === "string") {
84
+ JSON.parse(flags.json);
85
+ body = flags.json;
86
+ } else {
87
+ body = JSON.stringify(flags.json);
88
+ }
89
+ } catch (e) {
90
+ body = String(flags.json);
91
+ }
92
+ headers["Content-Type"] = "application/json";
93
+ headers["Content-Length"] = Buffer.byteLength(body);
94
+ } else if (flags.data) {
95
+ if (typeof flags.data === "string") {
96
+ const trimmed = flags.data.trim();
97
+ if (
98
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
99
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))
100
+ ) {
101
+ try {
102
+ JSON.parse(trimmed);
103
+ body = trimmed;
104
+ headers["Content-Type"] = "application/json";
105
+ } catch (e) {
106
+ body = flags.data;
107
+ headers["Content-Type"] ||= "application/x-www-form-urlencoded";
108
+ }
109
+ } else {
110
+ body = flags.data;
111
+ headers["Content-Type"] ||= "application/x-www-form-urlencoded";
112
+ }
113
+ } else {
114
+ body = JSON.stringify(flags.data);
115
+ headers["Content-Type"] = "application/json";
116
+ }
117
+ headers["Content-Length"] = Buffer.byteLength(body);
118
+ }
119
+
120
+ if (["GET", "HEAD", "DELETE"].includes(method)) {
121
+ body = null;
122
+ delete headers["Content-Length"];
123
+ }
124
+
125
+ if (!headers["Content-Type"] && body && typeof body === "string") {
126
+ const trimmed = body.trim();
127
+ if (
128
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
129
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))
130
+ ) {
131
+ try {
132
+ JSON.parse(trimmed);
133
+ headers["Content-Type"] = "application/json";
134
+ } catch (e) {
135
+ headers["Content-Type"] = "text/plain";
136
+ }
137
+ } else {
138
+ headers["Content-Type"] = "text/plain";
139
+ }
140
+ }
141
+
142
+ const options = {
143
+ hostname: parsed.hostname,
144
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
145
+ path: parsed.pathname + parsed.search,
146
+ method,
147
+ headers: {
148
+ "User-Agent": "curl/8.x-clone",
149
+ Accept: "*/*",
150
+ ...headers,
151
+ },
152
+ lookup: createDnsLookup(flags),
153
+ };
154
+
155
+ if (protocol === https) {
156
+ options.agent = new https.Agent({
157
+ rejectUnauthorized: !flags.insecure,
158
+ });
159
+ }
160
+
161
+ const cookies = loadCookieJar(flags);
162
+ if (cookies.length > 0) {
163
+ const cookieHeader = cookies
164
+ .filter(c => !c.expireTime || c.expireTime > Date.now())
165
+ .map(c => `${c.name}=${c.value}`)
166
+ .join("; ");
167
+ if (cookieHeader) {
168
+ options.headers["Cookie"] = cookieHeader;
169
+ }
170
+ }
171
+
172
+ verboseRequest(flags, options, body);
173
+
174
+ const req = protocol.request(options, (res) => {
175
+ if (flags.cookieJar) {
176
+ const setCookies = res.headers["set-cookie"];
177
+ if (setCookies) {
178
+ saveCookieJar(flags.cookieJar, setCookies, parsed.hostname);
179
+ }
180
+ }
181
+
182
+ if (flags.verbose) {
183
+ verboseResponse(flags, res, "");
184
+ }
185
+
186
+ if (flags.location && [301, 302, 303, 307, 308].includes(res.statusCode)) {
187
+ const maxRedirs = flags.maxRedirs ?? Infinity;
188
+ if (redirectCount >= maxRedirs) {
189
+ return reject(new Error(`Too many redirects (max: ${maxRedirs})`));
190
+ }
191
+ const newUrl = res.headers.location;
192
+ if (newUrl) {
193
+ const redirectUrl = new URL(newUrl, url).toString();
194
+ verboseLog(flags, `> Redirecting to: ${redirectUrl} (${redirectCount + 1}/${maxRedirs === Infinity ? '∞' : maxRedirs})`);
195
+ return resolve(makeRequest(redirectUrl, method, flags, redirectCount + 1));
196
+ }
197
+ }
198
+
199
+ if (flags.head || flags.include || method === "HEAD") {
200
+ let out = `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}\n`;
201
+ for (const [k, v] of Object.entries(res.headers)) {
202
+ out += `${k}: ${v}\n`;
203
+ }
204
+ out += "\n";
205
+ process.stdout.write(out);
206
+ if (flags.head || method === "HEAD") {
207
+ return resolve();
208
+ }
209
+ }
210
+
211
+ const isBinary = isBinaryResponse(res);
212
+
213
+ if (
214
+ isBinary ||
215
+ flags.output ||
216
+ flags.outputDir ||
217
+ flags.maxFilesize ||
218
+ flags.speedLimit ||
219
+ flags.speedTime
220
+ ) {
221
+ const dest = getStreamDestination(res, flags, parsed, isBinary);
222
+ streamResponse(res, dest, flags);
223
+ res.on("end", resolve);
224
+ } else {
225
+ bufferResponse(res, flags, resolve);
226
+ }
227
+ });
228
+
229
+ req.on("error", reject);
230
+
231
+ if (flags.timeout) {
232
+ req.setTimeout(flags.timeout * 1000, () => {
233
+ req.destroy(new Error("Request timeout"));
234
+ });
235
+ }
236
+
237
+ if (body) {
238
+ if (Buffer.isBuffer(body)) {
239
+ req.write(body);
240
+ } else if (typeof body === "string") {
241
+ req.write(body, "utf8");
242
+ } else {
243
+ req.write(String(body));
244
+ }
245
+ }
246
+
247
+ req.end();
248
+ });
249
+ }
250
+
251
+ export function request(url, method = "GET", flags = {}) {
252
+ const maxRetries = flags.retry || 0;
253
+ let lastError;
254
+ let attempt = 0;
255
+
256
+ async function attemptRequest() {
257
+ attempt++;
258
+ try {
259
+ return await makeRequest(url, method, flags);
260
+ } catch (err) {
261
+ lastError = err;
262
+
263
+ const shouldRetry =
264
+ maxRetries > 0 &&
265
+ attempt <= maxRetries &&
266
+ (RETRY_CODES.includes(err.code) || err.message?.includes("ECONNREFUSED"));
267
+
268
+ if (shouldRetry) {
269
+ verboseLog(flags, `> Retry ${attempt}/${maxRetries} after error: ${err.message}`);
270
+ await sleep(1000 * attempt);
271
+ return attemptRequest();
272
+ }
273
+
274
+ throw err;
275
+ }
276
+ }
277
+
278
+ return attemptRequest();
279
+ }
280
+
281
+ export async function parallelRequests(urls, flags = {}) {
282
+ if (!Array.isArray(urls) || urls.length === 0) {
283
+ throw new Error("--parallel requires multiple URLs");
284
+ }
285
+
286
+ const max = flags.parallelMax || urls.length;
287
+ let index = 0;
288
+
289
+ await mayWait(flags);
290
+ async function worker() {
291
+ while (true) {
292
+ const urlIndex = index++;
293
+ if (urlIndex >= max || urlIndex >= urls.length) return;
294
+ const url = urls[urlIndex];
295
+
296
+ try {
297
+ await request(url, "GET", flags);
298
+ } catch (err) {
299
+ console.error(`Error: ${url} → ${err.message}`);
300
+ }
301
+ }
302
+ }
303
+
304
+ const workers = [];
305
+ for (let i = 0; i < Math.min(max, urls.length); i++) {
306
+ workers.push(worker());
307
+ }
308
+
309
+ await Promise.all(workers);
310
+ }
311
+
312
+ export const get = (url, flags) => request(url, "GET", flags);
313
+ export const post = (url, flags) => request(url, "POST", flags);
314
+ export const put = (url, flags) => request(url, "PUT", flags);
315
+ export const patch = (url, flags) => request(url, "PATCH", flags);
316
+ export const deleteReq = (url, flags) => request(url, "DELETE", flags);
package/lib/monitor.js ADDED
@@ -0,0 +1,61 @@
1
+ import { Transform } from "stream";
2
+
3
+ export function createSpeedMonitorStream(minSpeed, duration) {
4
+ // If no limits set, return a pass-through
5
+ if (!minSpeed || !duration) {
6
+ return new Transform({
7
+ transform(chunk, encoding, callback) {
8
+ callback(null, chunk);
9
+ },
10
+ });
11
+ }
12
+
13
+ let currentBytes = 0;
14
+ let belowLimitSeconds = 0;
15
+ let isActive = true;
16
+
17
+ const monitor = new Transform({
18
+ transform(chunk, encoding, callback) {
19
+ currentBytes += chunk.length;
20
+ callback(null, chunk);
21
+ },
22
+ flush(callback) {
23
+ isActive = false;
24
+ clearInterval(intervalId);
25
+ callback();
26
+ },
27
+ });
28
+
29
+ const intervalId = setInterval(() => {
30
+ if (!isActive) {
31
+ clearInterval(intervalId);
32
+ return;
33
+ }
34
+
35
+ // Check speed for this second
36
+ const speed = currentBytes; // since interval is 1s
37
+ if (speed < minSpeed) {
38
+ belowLimitSeconds++;
39
+ } else {
40
+ belowLimitSeconds = 0;
41
+ }
42
+
43
+ // Reset counter for next second
44
+ currentBytes = 0;
45
+
46
+ if (belowLimitSeconds >= duration) {
47
+ clearInterval(intervalId);
48
+ monitor.destroy(
49
+ new Error(
50
+ `Operation too slow. Less than ${minSpeed} bytes/sec transferred the last ${duration} seconds`,
51
+ ),
52
+ );
53
+ }
54
+ }, 1000);
55
+
56
+ // Clean up on error or close
57
+ monitor.on("close", () => clearInterval(intervalId));
58
+ monitor.on("error", () => clearInterval(intervalId));
59
+
60
+ return monitor;
61
+ }