mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[bugfix] implement rety strategie for ip families
This commit is contained in:
@@ -141,6 +141,7 @@ export class SongDetailsCacheService {
|
||||
|
||||
if(error) {
|
||||
lastError = error;
|
||||
log.error("Failed to download SongDetailCache file", etag, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -152,7 +153,6 @@ export class SongDetailsCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
log.error("Failed to download SongDetailCache file", etag, lastError);
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { createWriteStream } from "fs";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { Observable, shareReplay, tap } from "rxjs";
|
||||
import log from "electron-log";
|
||||
import got, { Options } from "got";
|
||||
import { IncomingHttpHeaders, IncomingMessage } from "http";
|
||||
import { unlinkSync } from "fs-extra";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import path from "path";
|
||||
import { pipeline } from "stream/promises";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { LookupAddress, LookupAllOptions, LookupOptions } from "dns";
|
||||
import dns from "node:dns";
|
||||
import { createWriteStream, WriteStream } from 'fs';
|
||||
import { Progression } from 'main/helpers/fs.helpers';
|
||||
import { Observable } from 'rxjs';
|
||||
import { shareReplay, tap } from 'rxjs/operators';
|
||||
import log from 'electron-log';
|
||||
import got from 'got';
|
||||
import { IncomingHttpHeaders, IncomingMessage } from 'http';
|
||||
import { unlinkSync } from 'fs-extra';
|
||||
import { tryit } from 'shared/helpers/error.helpers';
|
||||
import path from 'path';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import internal from 'stream';
|
||||
|
||||
export class RequestService {
|
||||
private static instance: RequestService;
|
||||
private preferredFamily: number | undefined = undefined;
|
||||
|
||||
public static getInstance(): RequestService {
|
||||
if (!RequestService.instance) {
|
||||
@@ -24,12 +25,169 @@ export class RequestService {
|
||||
|
||||
private constructor() {}
|
||||
|
||||
private dnslookup( hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void,): void {
|
||||
console.log("OPTINOS", options);
|
||||
const checkAllOptions: LookupAllOptions = {...(options ?? {}), all: true};
|
||||
dns.lookup(hostname, checkAllOptions, (err: Error, adresses: LookupAddress[]) => {
|
||||
callback(err, adresses);
|
||||
});
|
||||
public async getJSON<T = unknown>(url: string): Promise<{ data: T; headers: IncomingHttpHeaders }> {
|
||||
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
for (const family of familiesToTry) {
|
||||
try {
|
||||
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
const res = await got(url, { dnsLookupIpVersion: family, responseType: 'json' });
|
||||
this.preferredFamily = family;
|
||||
return { data: res.body as T, headers: res.headers };
|
||||
} catch (err) {
|
||||
log.warn(`IPv${family} request failed, trying next one... URL: ${url}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
log.error(`IPv4 and IPv6 requests failed for URL: ${url}`);
|
||||
this.preferredFamily = undefined;
|
||||
throw new Error(`IPv4 and IPv6 requests failed for URL: ${url}`);
|
||||
}
|
||||
|
||||
public downloadFile(
|
||||
url: string,
|
||||
dest: string,
|
||||
opt?: { preferContentDisposition?: boolean }
|
||||
): Observable<Progression<string>> {
|
||||
return new Observable<Progression<string>>((subscriber) => {
|
||||
const progress: Progression<string> = { current: 0, total: 0 };
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
let attempt = 0;
|
||||
let stream: got.GotEmitter & internal.Duplex;
|
||||
|
||||
const tryNextFamily = () => {
|
||||
if (attempt >= familiesToTry.length) {
|
||||
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const family = familiesToTry[attempt++];
|
||||
let file: WriteStream | undefined;
|
||||
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
stream = got.stream(url, { dnsLookupIpVersion: family });
|
||||
|
||||
stream.on('response', (response) => {
|
||||
this.preferredFamily = family;
|
||||
|
||||
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
|
||||
|
||||
if (filename) {
|
||||
dest = path.join(path.dirname(dest), sanitize(filename));
|
||||
}
|
||||
|
||||
progress.data = dest;
|
||||
file = createWriteStream(dest);
|
||||
|
||||
pipeline(stream, file).catch(err => {
|
||||
file?.destroy();
|
||||
tryit(() => unlinkSync(dest));
|
||||
subscriber.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('downloadProgress', ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
stream.on('error', err => {
|
||||
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
|
||||
stream.destroy();
|
||||
file?.destroy();
|
||||
tryNextFamily();
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
file?.end();
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
};
|
||||
|
||||
tryNextFamily();
|
||||
|
||||
return () => {
|
||||
stream?.destroy();
|
||||
};
|
||||
}).pipe(
|
||||
tap({ error: (e) => log.error(e, url, dest) }),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
public downloadBuffer(
|
||||
url: string,
|
||||
options?: got.GotOptions<null>
|
||||
): Observable<Progression<Buffer, IncomingMessage>> {
|
||||
return new Observable<Progression<Buffer, IncomingMessage>>((subscriber) => {
|
||||
const progress: Progression<Buffer, IncomingMessage> = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
data: null,
|
||||
};
|
||||
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
let attempt = 0;
|
||||
let stream: got.GotEmitter & internal.Duplex;
|
||||
|
||||
const tryNextFamily = () => {
|
||||
if (attempt >= familiesToTry.length) {
|
||||
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const family = familiesToTry[attempt++];
|
||||
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
stream = got.stream(url, { dnsLookupIpVersion: family, ...(options ?? {}) });
|
||||
|
||||
let data = Buffer.alloc(0);
|
||||
let response: IncomingMessage;
|
||||
|
||||
stream.once('response', (res) => {
|
||||
this.preferredFamily = family;
|
||||
response = res;
|
||||
});
|
||||
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
data = Buffer.concat([data, chunk]);
|
||||
});
|
||||
|
||||
stream.on('downloadProgress', ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
stream.on('error', err => {
|
||||
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
|
||||
stream.destroy();
|
||||
tryNextFamily();
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
progress.data = data;
|
||||
progress.extra = response;
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
};
|
||||
|
||||
tryNextFamily();
|
||||
|
||||
return () => {
|
||||
stream?.destroy();
|
||||
};
|
||||
}).pipe(
|
||||
tap({ error: (e) => log.error(e, url) }),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
public getFilenameFromContentDisposition(disposition: string): string | undefined {
|
||||
@@ -55,109 +213,4 @@ export class RequestService {
|
||||
const partialDisposition = disposition.slice(filenameStart);
|
||||
return asciiFilenameRegex.exec(partialDisposition)?.[2];
|
||||
}
|
||||
|
||||
public async getJSON<T = unknown>(url: string): Promise<{ data: T, headers: IncomingHttpHeaders }> {
|
||||
|
||||
try{
|
||||
const res = await got(url, { dnsLookup: this.dnslookup });
|
||||
return { data: JSON.parse(res.body), headers: res.headers };
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public downloadFile(url: string, dest: string, opt?:{ preferContentDisposition?: boolean }): Observable<Progression<string>> {
|
||||
return new Observable<Progression<string>>(subscriber => {
|
||||
const progress: Progression<string> = { current: 0, total: 0 };
|
||||
|
||||
const stream = got.stream(url, { dnsLookup: this.dnslookup })
|
||||
|
||||
stream.on("response", response => {
|
||||
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers["content-disposition"]) : null;
|
||||
|
||||
if (filename) {
|
||||
dest = path.join(path.dirname(dest), sanitize(filename));
|
||||
}
|
||||
|
||||
progress.data = dest;
|
||||
|
||||
const file = createWriteStream(dest);
|
||||
|
||||
pipeline(stream, file).catch(err => {
|
||||
subscriber.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on("downloadProgress", ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
stream.on("error", err => {
|
||||
tryit(() => unlinkSync(dest));
|
||||
subscriber.error(err);
|
||||
});
|
||||
|
||||
stream.on("end", () => {
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
|
||||
return () => {
|
||||
stream.destroy();
|
||||
}
|
||||
|
||||
}).pipe(tap({ error: e => log.error(e, url, dest) }), shareReplay(1));
|
||||
}
|
||||
|
||||
public downloadBuffer(url: string, options?: Options & { isStream?: true }): Observable<Progression<Buffer, IncomingMessage>> {
|
||||
return new Observable<Progression<Buffer, IncomingMessage>>(subscriber => {
|
||||
const progress: Progression<Buffer, IncomingMessage> = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
data: null,
|
||||
};
|
||||
|
||||
const _options = {...(options ?? {}), dnsLookup: this.dnslookup}
|
||||
|
||||
const req = got.stream(url, _options);
|
||||
|
||||
let data = Buffer.alloc(0);
|
||||
let response: IncomingMessage;
|
||||
|
||||
req.once("response", res => {
|
||||
response = res;
|
||||
});
|
||||
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
data = Buffer.concat([data, chunk]);
|
||||
})
|
||||
|
||||
req.on("downloadProgress", ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
req.once("error", err => {
|
||||
subscriber.error(err);
|
||||
});
|
||||
|
||||
req.once("end", () => {
|
||||
progress.data = data;
|
||||
progress.extra = response;
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
|
||||
req.resume();
|
||||
|
||||
return () => {
|
||||
req.destroy();
|
||||
}
|
||||
|
||||
}).pipe(tap({ error: log.error }), shareReplay(1))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user