Merge branch 'develop' into feat/scylladb
This commit is contained in:
commit
76b383dee1
18 changed files with 392 additions and 74 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
||||||
# Visual Studio Code
|
# Visual Studio Code
|
||||||
/.vscode
|
/.vscode
|
||||||
!/.vscode/extensions.json
|
!/.vscode/extensions.json
|
||||||
|
!/.vscode/launch.json
|
||||||
|
|
||||||
# Intelij-IDEA
|
# Intelij-IDEA
|
||||||
/.idea
|
/.idea
|
||||||
|
|
|
@ -38,7 +38,7 @@ testCommit:
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
- pnpm i --frozen-lockfile
|
- pnpm i --frozen-lockfile
|
||||||
- pnpm run build
|
- pnpm run build:debug
|
||||||
- pnpm run migrate
|
- pnpm run migrate
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
|
@ -66,4 +66,4 @@ cache:
|
||||||
- node_modules/
|
- node_modules/
|
||||||
- packages/*/node_modules/
|
- packages/*/node_modules/
|
||||||
- packages/backend/native-utils/node_modules/
|
- packages/backend/native-utils/node_modules/
|
||||||
- packages/backend/native-utils/.cargo/
|
- packages/backend/native-utils/target/
|
||||||
|
|
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch PNPM Debug Script",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"runtimeExecutable": "pnpm",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run", "debug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -67,7 +67,7 @@ Closes #10313
|
||||||
|
|
||||||
- Fix: :bug: don't convert time since epoch for ratelimit
|
- Fix: :bug: don't convert time since epoch for ratelimit
|
||||||
|
|
||||||
https://calckey.social/notes/9gkasnzglmi07rpa
|
https://firefish.social/notes/9gkasnzglmi07rpa
|
||||||
|
|
||||||
- Fix: :bug: fix tapping parent on mobile causing side effects
|
- Fix: :bug: fix tapping parent on mobile causing side effects
|
||||||
|
|
||||||
|
@ -1834,7 +1834,7 @@ Closes #9843
|
||||||
|
|
||||||
- Fix: :recycle: use locale for error
|
- Fix: :recycle: use locale for error
|
||||||
|
|
||||||
https://calckey.social/notes/9fippqiwhl287b5m
|
https://firefish.social/notes/9fippqiwhl287b5m
|
||||||
|
|
||||||
- Fix mfm-cheat-sheet styling
|
- Fix mfm-cheat-sheet styling
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
"dev": "pnpm node ./scripts/dev.js",
|
"dev": "pnpm node ./scripts/dev.js",
|
||||||
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
||||||
"lint": "pnpm -r --parallel run lint",
|
"lint": "pnpm -r --parallel run lint",
|
||||||
|
"debug": "pnpm run build:debug && pnpm run start",
|
||||||
|
"build:debug": "pnpm -r --parallel run build:debug && pnpm run gulp",
|
||||||
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||||
"cy:run": "cypress run",
|
"cy:run": "cypress run",
|
||||||
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
"build": "pnpm run build:napi && pnpm run build:migration",
|
"build": "pnpm run build:napi && pnpm run build:migration",
|
||||||
"build:napi": "napi build --features napi --platform --release ./built/",
|
"build:napi": "napi build --features napi --platform --release ./built/",
|
||||||
"build:migration": "cargo build --locked --release --manifest-path ./migration/Cargo.toml && cp ./target/release/migration ./built/migration",
|
"build:migration": "cargo build --locked --release --manifest-path ./migration/Cargo.toml && cp ./target/release/migration ./built/migration",
|
||||||
"build:debug": "napi build --platform ./built/ && cargo build --manifest-path ./migration/Cargo.toml",
|
"build:debug": "napi build --features napi --platform ./built/ && cargo build --locked --manifest-path ./migration/Cargo.toml && cp -v ./target/debug/migration ./built/migration",
|
||||||
"prepublishOnly": "napi prepublish -t npm",
|
"prepublishOnly": "napi prepublish -t npm",
|
||||||
"test": "pnpm run cargo:test && pnpm run build:napi && ava",
|
"test": "pnpm run cargo:test && pnpm run build:napi && ava",
|
||||||
"universal": "napi universal",
|
"universal": "napi universal",
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"revertmigration:scylla": "cargo run --release --locked --manifest-path ./native-utils/scylla-migration/Cargo.toml -- -d ./native-utils/scylla-migration/cql -c ../../.config/default.yml down",
|
"revertmigration:scylla": "cargo run --release --locked --manifest-path ./native-utils/scylla-migration/Cargo.toml -- -d ./native-utils/scylla-migration/cql -c ../../.config/default.yml down",
|
||||||
"check:connect": "node ./check_connect.js",
|
"check:connect": "node ./check_connect.js",
|
||||||
"build": "pnpm swc src -d built -D",
|
"build": "pnpm swc src -d built -D",
|
||||||
|
"build:debug": "pnpm swc src -d built -s -D",
|
||||||
"watch": "pnpm swc src -d built -D -w",
|
"watch": "pnpm swc src -d built -D -w",
|
||||||
"lint": "pnpm rome check --apply *",
|
"lint": "pnpm rome check --apply *",
|
||||||
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
||||||
|
|
221
packages/backend/src/server/file/byte-range-readable.ts
Normal file
221
packages/backend/src/server/file/byte-range-readable.ts
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import { Readable, ReadableOptions } from "node:stream";
|
||||||
|
import { Buffer, constants as BufferConstants } from "node:buffer";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
|
||||||
|
interface ByteRange {
|
||||||
|
start: bigint;
|
||||||
|
end: bigint;
|
||||||
|
size: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOUNDARY_CHARS =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
const BYTERANGE_SPEC_REGEX = /^bytes=(.+)$/;
|
||||||
|
const BYTERANGE_REGEX = /(\d*)-(\d*)/;
|
||||||
|
|
||||||
|
const BIGINT_0 = BigInt(0);
|
||||||
|
const BIGINT_1 = BigInt(1);
|
||||||
|
const BOUNDARY_SIZE = 40;
|
||||||
|
|
||||||
|
function extractRanges(
|
||||||
|
fileSize: bigint,
|
||||||
|
maxByteRanges: number,
|
||||||
|
rangeHeaderValue: string,
|
||||||
|
): ByteRange[] {
|
||||||
|
const ranges: ByteRange[] = [];
|
||||||
|
|
||||||
|
if (!rangeHeaderValue) return ranges;
|
||||||
|
|
||||||
|
const rangeSpecMatch = rangeHeaderValue.match(BYTERANGE_SPEC_REGEX);
|
||||||
|
if (!rangeSpecMatch) return [];
|
||||||
|
|
||||||
|
const rangeSpecs = rangeSpecMatch[1].split(",");
|
||||||
|
for (let i = 0; i < rangeSpecs.length; i = i + 1) {
|
||||||
|
const byteRange = rangeSpecs[i].match(BYTERANGE_REGEX);
|
||||||
|
if (!byteRange) return [];
|
||||||
|
|
||||||
|
let start: bigint;
|
||||||
|
let end: bigint;
|
||||||
|
let size: bigint;
|
||||||
|
|
||||||
|
if (byteRange[1]) {
|
||||||
|
start = BigInt(byteRange[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteRange[2]) {
|
||||||
|
end = BigInt(byteRange[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start === undefined && end === undefined) {
|
||||||
|
/* some invalid range like bytes=- */
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start === undefined) {
|
||||||
|
/* end-of-file range like -500 */
|
||||||
|
start = fileSize - end;
|
||||||
|
end = fileSize - BIGINT_1;
|
||||||
|
if (start < BIGINT_0) return []; /* range larger than file, return */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end === undefined) {
|
||||||
|
/* range like 0- */
|
||||||
|
end = fileSize - BIGINT_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start > end || end >= fileSize) {
|
||||||
|
/* return empty range to issue regular 200 */
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
size = end - start + BIGINT_1;
|
||||||
|
|
||||||
|
if (1 > maxByteRanges - ranges.length) return [];
|
||||||
|
|
||||||
|
ranges.push({
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
size: size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBoundary(len: number): string {
|
||||||
|
let chars = [];
|
||||||
|
for (let i = 0; i < len; i = i + 1) {
|
||||||
|
chars[i] = BOUNDARY_CHARS.charAt(
|
||||||
|
Math.floor(Math.random() * BOUNDARY_CHARS.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return chars.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
class ByteRangeReadable extends Readable {
|
||||||
|
size: bigint; /* the total size in bytes */
|
||||||
|
boundary: string; /* boundary marker to use in multipart headers */
|
||||||
|
|
||||||
|
private fd: number;
|
||||||
|
private ranges: ByteRange[];
|
||||||
|
private index: number; /* index within ranges */
|
||||||
|
private position: bigint;
|
||||||
|
private end: bigint;
|
||||||
|
private contentType: string;
|
||||||
|
private fileSize: bigint;
|
||||||
|
private headers: Buffer[];
|
||||||
|
private trailer: Buffer;
|
||||||
|
|
||||||
|
static parseByteRanges(
|
||||||
|
fileSize: bigint,
|
||||||
|
maxByteRanges: number,
|
||||||
|
rangeHeaderValue?: string,
|
||||||
|
): ByteRange[] {
|
||||||
|
return extractRanges(fileSize, maxByteRanges, rangeHeaderValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPartHeader(range: ByteRange): Buffer {
|
||||||
|
return Buffer.from(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
`--${this.boundary}`,
|
||||||
|
`Content-Type: ${this.contentType}`,
|
||||||
|
`Content-Range: bytes ${range.start}-${range.end}/${this.fileSize}`,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
].join("\r\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
fd: number,
|
||||||
|
fileSize: bigint,
|
||||||
|
ranges: ByteRange[],
|
||||||
|
contentType: string,
|
||||||
|
opts?: ReadableOptions,
|
||||||
|
) {
|
||||||
|
super(opts);
|
||||||
|
|
||||||
|
if (ranges.length === 0) {
|
||||||
|
throw Error("this requires at least 1 byte range");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fd = fd;
|
||||||
|
this.ranges = ranges;
|
||||||
|
this.fileSize = fileSize;
|
||||||
|
this.contentType = contentType;
|
||||||
|
|
||||||
|
this.position = BIGINT_1;
|
||||||
|
this.end = BIGINT_0;
|
||||||
|
this.index = -1;
|
||||||
|
this.headers = [];
|
||||||
|
|
||||||
|
this.size = BIGINT_0;
|
||||||
|
|
||||||
|
if (this.ranges.length === 1) {
|
||||||
|
this.size = this.ranges[0].size;
|
||||||
|
} else {
|
||||||
|
this.boundary = createBoundary(BOUNDARY_SIZE);
|
||||||
|
this.ranges.forEach((r) => {
|
||||||
|
const header = this.createPartHeader(r);
|
||||||
|
this.headers.push(header);
|
||||||
|
|
||||||
|
this.size += BigInt(header.length) + r.size;
|
||||||
|
});
|
||||||
|
this.trailer = Buffer.from(`\r\n--${this.boundary}--\r\n`);
|
||||||
|
this.size += BigInt(this.trailer.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_read(n) {
|
||||||
|
if (this.index == this.ranges.length) {
|
||||||
|
this.push(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.position > this.end) {
|
||||||
|
/* move ahead to the next index */
|
||||||
|
this.index++;
|
||||||
|
|
||||||
|
if (this.index === this.ranges.length) {
|
||||||
|
if (this.trailer) {
|
||||||
|
this.push(this.trailer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.push(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.position = this.ranges[this.index].start;
|
||||||
|
this.end = this.ranges[this.index].end;
|
||||||
|
|
||||||
|
if (this.ranges.length > 1) {
|
||||||
|
this.push(this.headers[this.index]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const max = this.end - this.position + BIGINT_1;
|
||||||
|
|
||||||
|
if (n > max) n = Number(max);
|
||||||
|
const buf = Buffer.alloc(n);
|
||||||
|
|
||||||
|
fs.read(this.fd, buf, 0, n, this.position, (err, bytesRead) => {
|
||||||
|
if (err) {
|
||||||
|
this.destroy(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bytesRead == 0) {
|
||||||
|
/* something seems to have gone wrong? */
|
||||||
|
this.push(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRead > n) bytesRead = n;
|
||||||
|
|
||||||
|
this.position += BigInt(bytesRead);
|
||||||
|
this.push(buf.slice(0, bytesRead));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ByteRange, ByteRangeReadable };
|
|
@ -14,6 +14,7 @@ import { detectType } from "@/misc/get-file-info.js";
|
||||||
import { convertToWebp } from "@/services/drive/image-processor.js";
|
import { convertToWebp } from "@/services/drive/image-processor.js";
|
||||||
import { GenerateVideoThumbnail } from "@/services/drive/generate-video-thumbnail.js";
|
import { GenerateVideoThumbnail } from "@/services/drive/generate-video-thumbnail.js";
|
||||||
import { StatusError } from "@/misc/fetch.js";
|
import { StatusError } from "@/misc/fetch.js";
|
||||||
|
import { ByteRangeReadable } from "./byte-range-readable.js";
|
||||||
import { FILE_TYPE_BROWSERSAFE } from "@/const.js";
|
import { FILE_TYPE_BROWSERSAFE } from "@/const.js";
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
|
@ -21,6 +22,8 @@ const _dirname = dirname(_filename);
|
||||||
|
|
||||||
const assets = `${_dirname}/../../server/file/assets/`;
|
const assets = `${_dirname}/../../server/file/assets/`;
|
||||||
|
|
||||||
|
const MAX_BYTE_RANGES = 10;
|
||||||
|
|
||||||
const commonReadableHandlerGenerator =
|
const commonReadableHandlerGenerator =
|
||||||
(ctx: Koa.Context) => (e: Error): void => {
|
(ctx: Koa.Context) => (e: Error): void => {
|
||||||
serverLogger.error(e);
|
serverLogger.error(e);
|
||||||
|
@ -122,31 +125,88 @@ export default async function (ctx: Koa.Context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let contentType;
|
||||||
|
let filename;
|
||||||
|
let fileHandle;
|
||||||
|
|
||||||
if (isThumbnail || isWebpublic) {
|
if (isThumbnail || isWebpublic) {
|
||||||
const { mime, ext } = await detectType(InternalStorage.resolvePath(key));
|
const { mime, ext } = await detectType(InternalStorage.resolvePath(key));
|
||||||
const filename = rename(file.name, {
|
(contentType = FILE_TYPE_BROWSERSAFE.includes(mime)
|
||||||
|
? mime
|
||||||
|
: "application/octet-stream"),
|
||||||
|
(filename = rename(file.name, {
|
||||||
suffix: isThumbnail ? "-thumb" : "-web",
|
suffix: isThumbnail ? "-thumb" : "-web",
|
||||||
extname: ext ? `.${ext}` : undefined,
|
extname: ext ? `.${ext}` : undefined,
|
||||||
}).toString();
|
}).toString());
|
||||||
|
|
||||||
ctx.body = InternalStorage.read(key);
|
fileHandle = await InternalStorage.open(key, "r");
|
||||||
ctx.set(
|
|
||||||
"Content-Type",
|
|
||||||
FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : "application/octet-stream",
|
|
||||||
);
|
|
||||||
ctx.set("Cache-Control", "max-age=31536000, immutable");
|
|
||||||
ctx.set("Content-Disposition", contentDisposition("inline", filename));
|
|
||||||
} else {
|
} else {
|
||||||
const readable = InternalStorage.read(file.accessKey!);
|
(contentType = FILE_TYPE_BROWSERSAFE.includes(file.type)
|
||||||
|
? file.type
|
||||||
|
: "application/octet-stream"),
|
||||||
|
(filename = file.name);
|
||||||
|
fileHandle = await InternalStorage.open(file.accessKey!, "r");
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can let Koa evaluate conditionals by setting
|
||||||
|
// the status to 200, along with the lastModified
|
||||||
|
// and etag properties, then checking ctx.fresh.
|
||||||
|
// Additionally, Range is ignored if a conditional GET would
|
||||||
|
// result in a 304 response, so we can return early here.
|
||||||
|
|
||||||
|
ctx.status = 200;
|
||||||
|
ctx.etag = file.md5;
|
||||||
|
ctx.lastModified = file.createdAt;
|
||||||
|
|
||||||
|
// When doing a conditional request, we MUST return a "Cache-Control" header
|
||||||
|
// if a normal 200 response would have included.
|
||||||
|
ctx.set("Cache-Control", "max-age=31536000, immutable");
|
||||||
|
|
||||||
|
if (ctx.fresh) {
|
||||||
|
ctx.status = 304;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.length = file.size;
|
||||||
|
ctx.set("Content-Disposition", contentDisposition("inline", filename));
|
||||||
|
ctx.set("Content-Type", contentType);
|
||||||
|
|
||||||
|
const ranges = ByteRangeReadable.parseByteRanges(
|
||||||
|
BigInt(file.size),
|
||||||
|
MAX_BYTE_RANGES,
|
||||||
|
ctx.headers["range"],
|
||||||
|
);
|
||||||
|
const readable =
|
||||||
|
ranges.length === 0
|
||||||
|
? fileHandle.createReadStream()
|
||||||
|
: new ByteRangeReadable(
|
||||||
|
fileHandle.fd,
|
||||||
|
BigInt(file.size),
|
||||||
|
ranges,
|
||||||
|
contentType,
|
||||||
|
);
|
||||||
readable.on("error", commonReadableHandlerGenerator(ctx));
|
readable.on("error", commonReadableHandlerGenerator(ctx));
|
||||||
ctx.body = readable;
|
ctx.body = readable;
|
||||||
|
|
||||||
|
if (ranges.length === 0) {
|
||||||
|
ctx.set("Accept-Ranges", "bytes");
|
||||||
|
} else {
|
||||||
|
ctx.status = 206;
|
||||||
|
ctx.length = readable.size;
|
||||||
|
readable.on("close", async () => {
|
||||||
|
await fileHandle.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ranges.length === 1) {
|
||||||
|
ctx.set(
|
||||||
|
"Content-Range",
|
||||||
|
`bytes ${ranges[0].start}-${ranges[0].end}/${file.size}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
ctx.set(
|
ctx.set(
|
||||||
"Content-Type",
|
"Content-Type",
|
||||||
FILE_TYPE_BROWSERSAFE.includes(file.type)
|
`multipart/byteranges; boundary=${readable.boundary}`,
|
||||||
? file.type
|
|
||||||
: "application/octet-stream",
|
|
||||||
);
|
);
|
||||||
ctx.set("Cache-Control", "max-age=31536000, immutable");
|
}
|
||||||
ctx.set("Content-Disposition", contentDisposition("inline", file.name));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
|
import * as fsPromises from "node:fs/promises";
|
||||||
import * as Path from "node:path";
|
import * as Path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
|
@ -13,6 +14,10 @@ export class InternalStorage {
|
||||||
public static resolvePath = (key: string) =>
|
public static resolvePath = (key: string) =>
|
||||||
Path.resolve(InternalStorage.path, key);
|
Path.resolve(InternalStorage.path, key);
|
||||||
|
|
||||||
|
public static open(key: string, flags: string) {
|
||||||
|
return fsPromises.open(InternalStorage.resolvePath(key), flags);
|
||||||
|
}
|
||||||
|
|
||||||
public static read(key: string) {
|
public static read(key: string) {
|
||||||
return fs.createReadStream(InternalStorage.resolvePath(key));
|
return fs.createReadStream(InternalStorage.resolvePath(key));
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,13 @@ async function getRelMeLinks(url: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const html = await getHtml(url);
|
const html = await getHtml(url);
|
||||||
const dom = new JSDOM(html);
|
const dom = new JSDOM(html);
|
||||||
const relMeLinks = [
|
const allLinks = [...dom.window.document.querySelectorAll("a, link")];
|
||||||
...dom.window.document.querySelectorAll("a[rel='me']"),
|
const relMeLinks = allLinks
|
||||||
...dom.window.document.querySelectorAll("link[rel='me']"),
|
.filter((a) => {
|
||||||
].map((a) => (a as HTMLAnchorElement | HTMLLinkElement).href);
|
const relAttribute = a.getAttribute("rel");
|
||||||
|
return relAttribute ? relAttribute.split(" ").includes("me") : false;
|
||||||
|
})
|
||||||
|
.map((a) => (a as HTMLAnchorElement | HTMLLinkElement).href);
|
||||||
return relMeLinks;
|
return relMeLinks;
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "pnpm vite build --watch --mode development",
|
"watch": "pnpm vite build --watch --mode development",
|
||||||
"build": "pnpm vite build",
|
"build": "pnpm vite build",
|
||||||
|
"build:debug": "pnpm run build",
|
||||||
"lint": "pnpm rome check **/*.ts --apply && pnpm run lint:vue",
|
"lint": "pnpm rome check **/*.ts --apply && pnpm run lint:vue",
|
||||||
"lint:vue": "pnpm paralint --ext .vue --fix '**/*.vue' --cache",
|
"lint:vue": "pnpm paralint --ext .vue --fix '**/*.vue' --cache",
|
||||||
"format": "pnpm rome format * --write && pnpm prettier --write '**/*.{scss,vue}' --cache --cache-strategy metadata"
|
"format": "pnpm rome format * --write && pnpm prettier --write '**/*.{scss,vue}' --cache --cache-strategy metadata"
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
:aria-label="media.comment"
|
:aria-label="media.comment"
|
||||||
preload="none"
|
preload="none"
|
||||||
controls
|
controls
|
||||||
|
playsinline
|
||||||
@contextmenu.stop
|
@contextmenu.stop
|
||||||
>
|
>
|
||||||
<source :src="media.url" :type="mediaType" />
|
<source :src="media.url" :type="mediaType" />
|
||||||
|
|
|
@ -127,8 +127,8 @@
|
||||||
</FormFolder>
|
</FormFolder>
|
||||||
<template #caption>{{
|
<template #caption>{{
|
||||||
i18n.t("_profile.metadataDescription", {
|
i18n.t("_profile.metadataDescription", {
|
||||||
a: "\<code\>\<a\>\</code\>",
|
a: "\<a\>",
|
||||||
l: "\<code\>\<a\>\</code\>",
|
l: "\<a\>",
|
||||||
rel: `rel="me" href="https://${host}/@${$i.username}"`,
|
rel: `rel="me" href="https://${host}/@${$i.username}"`,
|
||||||
})
|
})
|
||||||
}}</template>
|
}}</template>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"types": "./built/index.d.ts",
|
"types": "./built/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm swc src -d built -D",
|
"build": "pnpm swc src -d built -D",
|
||||||
|
"build:debug": "pnpm swc src -d built -s -D",
|
||||||
"render": "pnpm run build && pnpm run api && pnpm run api-prod && cp temp/firefish-js.api.json etc/ && pnpm run api-doc",
|
"render": "pnpm run build && pnpm run api && pnpm run api-prod && cp temp/firefish-js.api.json etc/ && pnpm run api-doc",
|
||||||
"tsd": "tsc && tsd",
|
"tsd": "tsc && tsd",
|
||||||
"api": "pnpm api-extractor run --local --verbose",
|
"api": "pnpm api-extractor run --local --verbose",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"typings": "./lib/src/index.d.ts",
|
"typings": "./lib/src/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p ./",
|
"build": "tsc -p ./",
|
||||||
|
"build:debug": "pnpm run build",
|
||||||
"lint": "pnpm rome check **/*.ts --apply",
|
"lint": "pnpm rome check **/*.ts --apply",
|
||||||
"format": "pnpm rome format --write src/**/*.ts",
|
"format": "pnpm rome format --write src/**/*.ts",
|
||||||
"doc": "typedoc --out ../docs ./src",
|
"doc": "typedoc --out ../docs ./src",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
|
"build:debug": "pnpm run build",
|
||||||
"watch": "pnpm swc src -d built -D -w",
|
"watch": "pnpm swc src -d built -D -w",
|
||||||
"lint": "pnpm rome check **/*.ts --apply",
|
"lint": "pnpm rome check **/*.ts --apply",
|
||||||
"format": "pnpm rome format * --write"
|
"format": "pnpm rome format * --write"
|
||||||
|
|
82
patrons.json
82
patrons.json
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"patrons": [
|
"patrons": [
|
||||||
"@atomicpoet@calckey.social",
|
"@atomicpoet@firefish.social",
|
||||||
"@shoq@mastodon.social",
|
"@shoq@mastodon.social",
|
||||||
"@pikadude@erisly.social",
|
"@pikadude@erisly.social",
|
||||||
"@sage@stop.voring.me",
|
"@sage@stop.voring.me",
|
||||||
"@sky@therian.club",
|
"@sky@therian.club",
|
||||||
"@panos@electricrequiem.com",
|
"@panos@electricrequiem.com",
|
||||||
"@redhunt07@www.foxyhole.io",
|
"@redhunt07@www.foxyhole.io",
|
||||||
"@griff@calckey.social",
|
"@griff@firefish.social",
|
||||||
"@cafkafk@ck.cafkafk.com",
|
"@cafkafk@ck.cafkafk.com",
|
||||||
"@privateger@plasmatrap.com",
|
"@privateger@plasmatrap.com",
|
||||||
"@effye@toot.thoughtworks.com",
|
"@effye@toot.thoughtworks.com",
|
||||||
|
@ -17,56 +17,56 @@
|
||||||
"@topher@mastodon.online",
|
"@topher@mastodon.online",
|
||||||
"@hanicef@stop.voring.me",
|
"@hanicef@stop.voring.me",
|
||||||
"@nmkj@calckey.jp",
|
"@nmkj@calckey.jp",
|
||||||
"@unattributed@calckey.social",
|
"@unattributed@firefish.social",
|
||||||
"@cody@misskey.codingneko.com",
|
"@cody@misskey.codingneko.com",
|
||||||
"@kate@blahaj.zone",
|
"@kate@blahaj.zone",
|
||||||
"@emtk@mkkey.net",
|
"@emtk@mkkey.net",
|
||||||
"@jovikowi@calckey.social",
|
"@jovikowi@firefish.social",
|
||||||
"@padraig@calckey.social",
|
"@padraig@firefish.social",
|
||||||
"@pancakes@cats.city",
|
"@pancakes@cats.city",
|
||||||
"@theresmiling@calckey.social",
|
"@theresmiling@firefish.social",
|
||||||
"@kristian@calckey.social",
|
"@kristian@firefish.social",
|
||||||
"@jo@blahaj.zone",
|
"@jo@blahaj.zone",
|
||||||
"@narF@calckey.social",
|
"@narF@firefish.social",
|
||||||
"@AlderForrest@raining.anvil.top",
|
"@AlderForrest@raining.anvil.top",
|
||||||
"@box464@calckey.social",
|
"@box464@firefish.social",
|
||||||
"@MariaTheMartian@calckey.social",
|
"@MariaTheMartian@firefish.social",
|
||||||
"@nisemikol@calckey.social",
|
"@nisemikol@firefish.social",
|
||||||
"@smallpatatas@blahaj.zone",
|
"@smallpatatas@blahaj.zone",
|
||||||
"@bayra@stop.voring.me",
|
"@bayra@stop.voring.me",
|
||||||
"@frost@wolfdo.gg",
|
"@frost@wolfdo.gg",
|
||||||
"@joebiden@fuckgov.org",
|
"@joebiden@fuckgov.org",
|
||||||
"@nyaa@calckey.social",
|
"@nyaa@firefish.social",
|
||||||
"@Dan@calckey.social",
|
"@Dan@firefish.social",
|
||||||
"@dana@calckey.social",
|
"@dana@firefish.social",
|
||||||
"@Jdreben@calckey.social",
|
"@Jdreben@firefish.social",
|
||||||
"@natalie@prismst.one",
|
"@natalie@prismst.one",
|
||||||
"@KelsonV@wandering.shop",
|
"@KelsonV@wandering.shop",
|
||||||
"@breakfastmtn@calckey.social",
|
"@breakfastmtn@firefish.social",
|
||||||
"@richardazia@mastodon.social",
|
"@richardazia@mastodon.social",
|
||||||
"@joestone@calckey.social",
|
"@joestone@firefish.social",
|
||||||
"@aj@calckey.social",
|
"@aj@firefish.social",
|
||||||
"@zepfanman@ramblingreaders.org",
|
"@zepfanman@ramblingreaders.org",
|
||||||
"@kimby@stop.voring.me",
|
"@kimby@stop.voring.me",
|
||||||
"@fyrfli@fyrfli.social",
|
"@fyrfli@fyrfli.social",
|
||||||
"@riversidebryan@firefish.lgbt",
|
"@riversidebryan@firefish.lgbt",
|
||||||
"@aRubes@sloth.run",
|
"@aRubes@sloth.run",
|
||||||
"@andreasdotorg@calckey.social",
|
"@andreasdotorg@firefish.social",
|
||||||
"@ozzy@calckey.online",
|
"@ozzy@calckey.online",
|
||||||
"@leni@windycity.style",
|
"@leni@windycity.style",
|
||||||
"@mhzmodels@calckey.art",
|
"@mhzmodels@calckey.art",
|
||||||
"@ReflexVE@calckey.social",
|
"@ReflexVE@firefish.social",
|
||||||
"@mark@calckey.social",
|
"@mark@firefish.social",
|
||||||
"@skyizwhite@himagine.club",
|
"@skyizwhite@himagine.club",
|
||||||
"@Uwu@calckey.social",
|
"@Uwu@firefish.social",
|
||||||
"@jGoose@calckey.social",
|
"@jGoose@firefish.social",
|
||||||
"@kunev@blewsky.social",
|
"@kunev@blewsky.social",
|
||||||
"@Simoto@electricrequiem.com",
|
"@Simoto@electricrequiem.com",
|
||||||
"@Evoterra@calckey.social",
|
"@Evoterra@firefish.social",
|
||||||
"@LauraLangdon@procial.tchncs.de",
|
"@LauraLangdon@procial.tchncs.de",
|
||||||
"@mho@social.heise.de",
|
"@mho@social.heise.de",
|
||||||
"@richardazia@calckey.social",
|
"@richardazia@firefish.social",
|
||||||
"@blues653@calckey.social",
|
"@blues653@firefish.social",
|
||||||
"@rafale_blue@calc.04.si",
|
"@rafale_blue@calc.04.si",
|
||||||
"@esm@lethallava.land",
|
"@esm@lethallava.land",
|
||||||
"@vmstan@vmst.io",
|
"@vmstan@vmst.io",
|
||||||
|
@ -74,33 +74,33 @@
|
||||||
"@renere@distance.blue",
|
"@renere@distance.blue",
|
||||||
"@theking@kitsunes.club",
|
"@theking@kitsunes.club",
|
||||||
"@toof@fedi.toofie.net",
|
"@toof@fedi.toofie.net",
|
||||||
"@Punko@calckey.social",
|
"@Punko@firefish.social",
|
||||||
"@joesbrat67@calckey.social",
|
"@joesbrat67@firefish.social",
|
||||||
"@arth@calckey.social",
|
"@arth@firefish.social",
|
||||||
"@octofloofy@ck.octofloofy.ink",
|
"@octofloofy@ck.octofloofy.ink",
|
||||||
"@pauliehedron@infosec.town",
|
"@pauliehedron@infosec.town",
|
||||||
"@soulthunk@lethallava.land",
|
"@soulthunk@lethallava.land",
|
||||||
"@bumble@ibe.social",
|
"@bumble@ibe.social",
|
||||||
"@DarrenNevares@calckey.social",
|
"@DarrenNevares@firefish.social",
|
||||||
"@irfan@calckey.social",
|
"@irfan@firefish.social",
|
||||||
"@dvd@dvd.chat",
|
"@dvd@dvd.chat",
|
||||||
"@charlie2alpha@electricrequiem.com",
|
"@charlie2alpha@electricrequiem.com",
|
||||||
"@arndot@layer8.space",
|
"@arndot@layer8.space",
|
||||||
"@ryan@c.ryanccn.dev",
|
"@ryan@c.ryanccn.dev",
|
||||||
"@lapastora_deprova@calckey.social",
|
"@lapastora_deprova@firefish.social",
|
||||||
"@rameez@calckey.social",
|
"@rameez@firefish.social",
|
||||||
"@dracoling@firetribe.org",
|
"@dracoling@firetribe.org",
|
||||||
"@Space6host@calckey.social",
|
"@Space6host@firefish.social",
|
||||||
"@zakalwe@plasmatrap.com",
|
"@zakalwe@plasmatrap.com",
|
||||||
"@seasicksailor@calckey.social",
|
"@seasicksailor@firefish.social",
|
||||||
"@geerue@calckey.social",
|
"@geerue@firefish.social",
|
||||||
"@WXFanatic@m.ai6yr.org",
|
"@WXFanatic@m.ai6yr.org",
|
||||||
"@Hunkabilly@calckey.world",
|
"@Hunkabilly@calckey.world",
|
||||||
"@samleegray@calckey.social",
|
"@samleegray@firefish.social",
|
||||||
"@schwarzewald@kodow.net",
|
"@schwarzewald@kodow.net",
|
||||||
"@Conatusprinciple@calckey.social",
|
"@Conatusprinciple@firefish.social",
|
||||||
"@183231bcb@firefish.lgbt",
|
"@183231bcb@firefish.lgbt",
|
||||||
"@wiase@calckey.social",
|
"@wiase@firefish.social",
|
||||||
"@leonieke@vitaulium.nl",
|
"@leonieke@vitaulium.nl",
|
||||||
"@soulfire@wackywolf.xyz",
|
"@soulfire@wackywolf.xyz",
|
||||||
"@elbullazul@pub.elbullazul.com",
|
"@elbullazul@pub.elbullazul.com",
|
||||||
|
@ -110,8 +110,8 @@
|
||||||
"@hryggrbyr@ibe.social"
|
"@hryggrbyr@ibe.social"
|
||||||
],
|
],
|
||||||
"sponsors": [
|
"sponsors": [
|
||||||
"@atomicpoet@calckey.social",
|
"@atomicpoet@firefish.social",
|
||||||
"@unattributed@calckey.social",
|
"@unattributed@firefish.social",
|
||||||
"@jtbennett@noc.social",
|
"@jtbennett@noc.social",
|
||||||
"\nInterkosmos Link"
|
"\nInterkosmos Link"
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue