mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-03-17 21:12:22 +01:00
Tested
This commit is contained in:
parent
6f28c3f293
commit
ef4ca27f4b
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
@ -58,6 +59,8 @@ namespace VoidCat.Controllers
|
||||
[HttpPost]
|
||||
[DisableRequestSizeLimit]
|
||||
[DisableFormValueModelBinding]
|
||||
[Authorize(AuthenticationSchemes = "Bearer,Nostr")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> UploadFile([FromQuery] bool cli = false)
|
||||
{
|
||||
try
|
||||
|
@ -95,7 +95,7 @@ public class UserManager
|
||||
}
|
||||
|
||||
var uid = await _store.LookupUser(user.Email);
|
||||
if (uid.HasValue)
|
||||
if (uid.HasValue && uid.Value != Guid.Empty)
|
||||
{
|
||||
var existingUser = await _store.Get(uid.Value);
|
||||
if (existingUser?.AuthType == UserAuthType.OAuth2)
|
||||
@ -119,7 +119,7 @@ public class UserManager
|
||||
public async ValueTask<User> LoginOrRegister(string pubkey)
|
||||
{
|
||||
var uid = await _store.LookupUser(pubkey);
|
||||
if (uid.HasValue)
|
||||
if (uid.HasValue && uid.Value != Guid.Empty)
|
||||
{
|
||||
var existingUser = await _store.Get(uid.Value);
|
||||
if (existingUser?.AuthType == UserAuthType.Nostr)
|
||||
|
@ -140,7 +140,7 @@ public static class VoidStartup
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.AddScheme<NostrAuthHandler>(NostrAuth.Scheme, "Nostr");
|
||||
o.AddScheme<NostrAuthHandler>(NostrAuth.Scheme, null);
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@void-cat/api",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.10",
|
||||
"description": "void.cat API package",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
@ -1,147 +1,192 @@
|
||||
import {
|
||||
AdminProfile, AdminUserListResult,
|
||||
ApiError, ApiKey,
|
||||
LoginSession,
|
||||
PagedRequest,
|
||||
PagedResponse,
|
||||
PaymentOrder,
|
||||
Profile, SetPaymentConfigRequest,
|
||||
SiteInfoResponse,
|
||||
VoidFileResponse
|
||||
AdminProfile,
|
||||
AdminUserListResult,
|
||||
ApiError,
|
||||
ApiKey,
|
||||
LoginSession,
|
||||
PagedRequest,
|
||||
PagedResponse,
|
||||
PaymentOrder,
|
||||
Profile,
|
||||
SetPaymentConfigRequest,
|
||||
SiteInfoResponse,
|
||||
VoidFileResponse,
|
||||
} from "./index";
|
||||
import { ProgressHandler, ProxyChallengeHandler, StateChangeHandler, VoidUploader } from "./upload";
|
||||
import {
|
||||
ProgressHandler,
|
||||
ProxyChallengeHandler,
|
||||
StateChangeHandler,
|
||||
VoidUploader,
|
||||
} from "./upload";
|
||||
import { StreamUploader } from "./stream-uploader";
|
||||
import { XHRUploader } from "./xhr-uploader";
|
||||
|
||||
export type AuthHandler = (url: string, method: string) => Promise<string>;
|
||||
|
||||
export class VoidApi {
|
||||
readonly #uri: string
|
||||
readonly #auth?: string
|
||||
readonly #scheme?: string
|
||||
readonly #uri: string;
|
||||
readonly #auth?: AuthHandler;
|
||||
|
||||
constructor(uri: string, auth?: string, scheme?: string) {
|
||||
this.#uri = uri;
|
||||
this.#auth = auth;
|
||||
this.#scheme = scheme;
|
||||
constructor(uri: string, auth?: AuthHandler) {
|
||||
this.#uri = uri;
|
||||
this.#auth = auth;
|
||||
}
|
||||
|
||||
async #req<T>(method: string, url: string, body?: object): Promise<T> {
|
||||
const absoluteUrl = `${this.#uri}${url}`;
|
||||
const headers: HeadersInit = {
|
||||
Accept: "application/json",
|
||||
};
|
||||
if (this.#auth) {
|
||||
headers["Authorization"] = await this.#auth(absoluteUrl, method);
|
||||
}
|
||||
if (body) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
async #req<T>(method: string, url: string, body?: object): Promise<T> {
|
||||
let headers: HeadersInit = {
|
||||
"Accept": "application/json"
|
||||
};
|
||||
if (this.#auth) {
|
||||
headers["Authorization"] = `${this.#scheme ?? "Bearer"} ${this.#auth}`;
|
||||
}
|
||||
if (body) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
const res = await fetch(`${this.#uri}${url}`, {
|
||||
method,
|
||||
headers,
|
||||
mode: "cors",
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
const text = await res.text();
|
||||
if (res.ok) {
|
||||
return text ? JSON.parse(text) as T : {} as T;
|
||||
} else {
|
||||
throw new ApiError(res.status, text);
|
||||
}
|
||||
const res = await fetch(absoluteUrl, {
|
||||
method,
|
||||
headers,
|
||||
mode: "cors",
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
if (res.ok) {
|
||||
return text ? (JSON.parse(text) as T) : ({} as T);
|
||||
} else {
|
||||
throw new ApiError(res.status, text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uploader for uploading files
|
||||
*/
|
||||
getUploader(
|
||||
file: File | Blob,
|
||||
stateChange?: StateChangeHandler,
|
||||
progress?: ProgressHandler,
|
||||
proxyChallenge?: ProxyChallengeHandler,
|
||||
chunkSize?: number
|
||||
): VoidUploader {
|
||||
if (StreamUploader.canUse()) {
|
||||
return new StreamUploader(this.#uri, file, stateChange, progress, proxyChallenge, this.#auth, chunkSize);
|
||||
} else {
|
||||
return new XHRUploader(this.#uri, file, stateChange, progress, proxyChallenge, this.#auth, chunkSize);
|
||||
}
|
||||
/**
|
||||
* Get uploader for uploading files
|
||||
*/
|
||||
getUploader(
|
||||
file: File | Blob,
|
||||
stateChange?: StateChangeHandler,
|
||||
progress?: ProgressHandler,
|
||||
proxyChallenge?: ProxyChallengeHandler,
|
||||
chunkSize?: number
|
||||
): VoidUploader {
|
||||
if (StreamUploader.canUse()) {
|
||||
return new StreamUploader(
|
||||
this.#uri,
|
||||
file,
|
||||
stateChange,
|
||||
progress,
|
||||
proxyChallenge,
|
||||
this.#auth,
|
||||
chunkSize
|
||||
);
|
||||
} else {
|
||||
return new XHRUploader(
|
||||
this.#uri,
|
||||
file,
|
||||
stateChange,
|
||||
progress,
|
||||
proxyChallenge,
|
||||
this.#auth,
|
||||
chunkSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General site information
|
||||
*/
|
||||
info() {
|
||||
return this.#req<SiteInfoResponse>("GET", "/info")
|
||||
}
|
||||
/**
|
||||
* General site information
|
||||
*/
|
||||
info() {
|
||||
return this.#req<SiteInfoResponse>("GET", "/info");
|
||||
}
|
||||
|
||||
fileInfo(id: string) {
|
||||
return this.#req<VoidFileResponse>("GET", `/upload/${id}`);
|
||||
}
|
||||
fileInfo(id: string) {
|
||||
return this.#req<VoidFileResponse>("GET", `/upload/${id}`);
|
||||
}
|
||||
|
||||
setPaymentConfig(id: string, cfg: SetPaymentConfigRequest) {
|
||||
return this.#req("POST", `/upload/${id}/payment`, cfg);
|
||||
}
|
||||
setPaymentConfig(id: string, cfg: SetPaymentConfigRequest) {
|
||||
return this.#req("POST", `/upload/${id}/payment`, cfg);
|
||||
}
|
||||
|
||||
createOrder(id: string) {
|
||||
return this.#req<PaymentOrder>("GET", `/upload/${id}/payment`);
|
||||
}
|
||||
createOrder(id: string) {
|
||||
return this.#req<PaymentOrder>("GET", `/upload/${id}/payment`);
|
||||
}
|
||||
|
||||
getOrder(file: string, order: string) {
|
||||
return this.#req<PaymentOrder>("GET", `/upload/${file}/payment/${order}`);
|
||||
}
|
||||
getOrder(file: string, order: string) {
|
||||
return this.#req<PaymentOrder>("GET", `/upload/${file}/payment/${order}`);
|
||||
}
|
||||
|
||||
login(username: string, password: string, captcha?: string) {
|
||||
return this.#req<LoginSession>("POST", `/auth/login`, { username, password, captcha });
|
||||
}
|
||||
login(username: string, password: string, captcha?: string) {
|
||||
return this.#req<LoginSession>("POST", `/auth/login`, {
|
||||
username,
|
||||
password,
|
||||
captcha,
|
||||
});
|
||||
}
|
||||
|
||||
register(username: string, password: string, captcha?: string) {
|
||||
return this.#req<LoginSession>("POST", `/auth/register`, { username, password, captcha });
|
||||
}
|
||||
register(username: string, password: string, captcha?: string) {
|
||||
return this.#req<LoginSession>("POST", `/auth/register`, {
|
||||
username,
|
||||
password,
|
||||
captcha,
|
||||
});
|
||||
}
|
||||
|
||||
getUser(id: string) {
|
||||
return this.#req<Profile>("GET", `/user/${id}`);
|
||||
}
|
||||
getUser(id: string) {
|
||||
return this.#req<Profile>("GET", `/user/${id}`);
|
||||
}
|
||||
|
||||
updateUser(u: Profile) {
|
||||
return this.#req<void>("POST", `/user/${u.id}`, u);
|
||||
}
|
||||
updateUser(u: Profile) {
|
||||
return this.#req<void>("POST", `/user/${u.id}`, u);
|
||||
}
|
||||
|
||||
listUserFiles(uid: string, pageReq: PagedRequest) {
|
||||
return this.#req<PagedResponse<VoidFileResponse>>("POST", `/user/${uid}/files`, pageReq);
|
||||
}
|
||||
listUserFiles(uid: string, pageReq: PagedRequest) {
|
||||
return this.#req<PagedResponse<VoidFileResponse>>(
|
||||
"POST",
|
||||
`/user/${uid}/files`,
|
||||
pageReq
|
||||
);
|
||||
}
|
||||
|
||||
submitVerifyCode(uid: string, code: string) {
|
||||
return this.#req<void>("POST", `/user/${uid}/verify`, { code });
|
||||
}
|
||||
submitVerifyCode(uid: string, code: string) {
|
||||
return this.#req<void>("POST", `/user/${uid}/verify`, { code });
|
||||
}
|
||||
|
||||
sendNewCode(uid: string) {
|
||||
return this.#req<void>("GET", `/user/${uid}/verify`);
|
||||
}
|
||||
sendNewCode(uid: string) {
|
||||
return this.#req<void>("GET", `/user/${uid}/verify`);
|
||||
}
|
||||
|
||||
updateFileMetadata(id: string, meta: any) {
|
||||
return this.#req<void>("POST", `/upload/${id}/meta`, meta);
|
||||
}
|
||||
updateFileMetadata(id: string, meta: any) {
|
||||
return this.#req<void>("POST", `/upload/${id}/meta`, meta);
|
||||
}
|
||||
|
||||
listApiKeys() {
|
||||
return this.#req<Array<ApiKey>>("GET", `/auth/api-key`);
|
||||
}
|
||||
listApiKeys() {
|
||||
return this.#req<Array<ApiKey>>("GET", `/auth/api-key`);
|
||||
}
|
||||
|
||||
createApiKey(req: any) {
|
||||
return this.#req<ApiKey>("POST", `/auth/api-key`, req);
|
||||
}
|
||||
createApiKey(req: any) {
|
||||
return this.#req<ApiKey>("POST", `/auth/api-key`, req);
|
||||
}
|
||||
|
||||
adminListFiles(pageReq: PagedRequest) {
|
||||
return this.#req<PagedResponse<VoidFileResponse>>("POST", "/admin/file", pageReq);
|
||||
}
|
||||
adminListFiles(pageReq: PagedRequest) {
|
||||
return this.#req<PagedResponse<VoidFileResponse>>(
|
||||
"POST",
|
||||
"/admin/file",
|
||||
pageReq
|
||||
);
|
||||
}
|
||||
|
||||
adminDeleteFile(id: string) {
|
||||
return this.#req<void>("DELETE", `/admin/file/${id}`);
|
||||
}
|
||||
adminDeleteFile(id: string) {
|
||||
return this.#req<void>("DELETE", `/admin/file/${id}`);
|
||||
}
|
||||
|
||||
adminUserList(pageReq: PagedRequest) {
|
||||
return this.#req<PagedResponse<AdminUserListResult>>("POST", `/admin/users`, pageReq);
|
||||
}
|
||||
adminUserList(pageReq: PagedRequest) {
|
||||
return this.#req<PagedResponse<AdminUserListResult>>(
|
||||
"POST",
|
||||
`/admin/users`,
|
||||
pageReq
|
||||
);
|
||||
}
|
||||
|
||||
adminUpdateUser(u: AdminProfile) {
|
||||
return this.#req<void>("POST", `/admin/update-user`, u);
|
||||
}
|
||||
}
|
||||
adminUpdateUser(u: AdminProfile) {
|
||||
return this.#req<void>("POST", `/admin/update-user`, u);
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ export class StreamUploader extends VoidUploader {
|
||||
highWaterMark: DefaultChunkSize
|
||||
});
|
||||
|
||||
const absoluteUrl = `${this.uri}/upload`;
|
||||
const reqHeaders = {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"V-Content-Type": !this.file.type ? "application/octet-stream" : this.file.type,
|
||||
@ -65,9 +66,9 @@ export class StreamUploader extends VoidUploader {
|
||||
reqHeaders["V-EncryptionParams"] = JSON.stringify(this.#encrypt!.getParams());
|
||||
}
|
||||
if (this.auth) {
|
||||
reqHeaders["Authorization"] = `Bearer ${this.auth}`;
|
||||
reqHeaders["Authorization"] = await this.auth(absoluteUrl, "POST");
|
||||
}
|
||||
const req = await fetch(`${this.uri}/upload`, {
|
||||
const req = await fetch(absoluteUrl, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
body: this.#encrypt ? rsBase.pipeThrough(this.#encrypt!.getEncryptionTransform()) : rsBase,
|
||||
|
@ -2,17 +2,19 @@ import { VoidUploadResult } from "./index";
|
||||
import sjcl from "sjcl";
|
||||
import { sjclcodec } from "./codecBytes";
|
||||
import { buf2hex } from "./utils";
|
||||
import { AuthHandler } from "./api";
|
||||
|
||||
/**
|
||||
* Generic upload state
|
||||
*/
|
||||
export enum UploadState {
|
||||
NotStarted,
|
||||
Starting,
|
||||
Hashing,
|
||||
Uploading,
|
||||
Done,
|
||||
Failed,
|
||||
Challenge
|
||||
NotStarted,
|
||||
Starting,
|
||||
Hashing,
|
||||
Uploading,
|
||||
Done,
|
||||
Failed,
|
||||
Challenge,
|
||||
}
|
||||
|
||||
export type StateChangeHandler = (s: UploadState) => void;
|
||||
@ -23,71 +25,71 @@ export type ProxyChallengeHandler = (html: string) => void;
|
||||
* Base file uploader class
|
||||
*/
|
||||
export abstract class VoidUploader {
|
||||
protected uri: string;
|
||||
protected file: File | Blob;
|
||||
protected auth?: string;
|
||||
protected maxChunkSize: number;
|
||||
protected onStateChange?: StateChangeHandler;
|
||||
protected onProgress?: ProgressHandler;
|
||||
protected onProxyChallenge?: ProxyChallengeHandler;
|
||||
protected uri: string;
|
||||
protected file: File | Blob;
|
||||
protected auth?: AuthHandler;
|
||||
protected maxChunkSize: number;
|
||||
protected onStateChange?: StateChangeHandler;
|
||||
protected onProgress?: ProgressHandler;
|
||||
protected onProxyChallenge?: ProxyChallengeHandler;
|
||||
|
||||
constructor(
|
||||
uri: string,
|
||||
file: File | Blob,
|
||||
stateChange?: StateChangeHandler,
|
||||
progress?: ProgressHandler,
|
||||
proxyChallenge?: ProxyChallengeHandler,
|
||||
auth?: string,
|
||||
chunkSize?: number
|
||||
) {
|
||||
this.uri = uri;
|
||||
this.file = file;
|
||||
this.onStateChange = stateChange;
|
||||
this.onProgress = progress;
|
||||
this.onProxyChallenge = proxyChallenge;
|
||||
this.auth = auth;
|
||||
this.maxChunkSize = chunkSize ?? Number.MAX_VALUE;
|
||||
constructor(
|
||||
uri: string,
|
||||
file: File | Blob,
|
||||
stateChange?: StateChangeHandler,
|
||||
progress?: ProgressHandler,
|
||||
proxyChallenge?: ProxyChallengeHandler,
|
||||
auth?: AuthHandler,
|
||||
chunkSize?: number
|
||||
) {
|
||||
this.uri = uri;
|
||||
this.file = file;
|
||||
this.onStateChange = stateChange;
|
||||
this.onProgress = progress;
|
||||
this.onProxyChallenge = proxyChallenge;
|
||||
this.auth = auth;
|
||||
this.maxChunkSize = chunkSize ?? Number.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256 hash the entire blob
|
||||
* @param file
|
||||
* @protected
|
||||
*/
|
||||
protected async digest(file: Blob) {
|
||||
const ChunkSize = 1024 * 1024;
|
||||
|
||||
// must compute hash in chunks, subtle crypto cannot hash files > 2Gb
|
||||
const sha = new sjcl.hash.sha256();
|
||||
let progress = 0;
|
||||
for (let x = 0; x < Math.ceil(file.size / ChunkSize); x++) {
|
||||
const offset = x * ChunkSize;
|
||||
const slice = file.slice(offset, offset + ChunkSize);
|
||||
const chunk = await slice.arrayBuffer();
|
||||
sha.update(sjclcodec.toBits(new Uint8Array(chunk)));
|
||||
this.onProgress?.((progress += chunk.byteLength));
|
||||
}
|
||||
return buf2hex(sjclcodec.fromBits(sha.finalize()));
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256 hash the entire blob
|
||||
* @param file
|
||||
* @protected
|
||||
*/
|
||||
protected async digest(file: Blob) {
|
||||
const ChunkSize = 1024 * 1024;
|
||||
/**
|
||||
* Upload a file to the API
|
||||
* @param headers any additional headers to send with the request
|
||||
*/
|
||||
abstract upload(headers?: HeadersInit): Promise<VoidUploadResult>;
|
||||
|
||||
// must compute hash in chunks, subtle crypto cannot hash files > 2Gb
|
||||
const sha = new sjcl.hash.sha256();
|
||||
let progress = 0;
|
||||
for (let x = 0; x < Math.ceil(file.size / ChunkSize); x++) {
|
||||
const offset = x * ChunkSize;
|
||||
const slice = file.slice(offset, offset + ChunkSize);
|
||||
const chunk = await slice.arrayBuffer();
|
||||
sha.update(sjclcodec.toBits(new Uint8Array(chunk)));
|
||||
this.onProgress?.(progress += chunk.byteLength);
|
||||
}
|
||||
return buf2hex(sjclcodec.fromBits(sha.finalize()));
|
||||
}
|
||||
/**
|
||||
* Can we use local encryption
|
||||
*/
|
||||
abstract canEncrypt(): boolean;
|
||||
|
||||
/**
|
||||
* Upload a file to the API
|
||||
* @param headers any additional headers to send with the request
|
||||
*/
|
||||
abstract upload(headers?: HeadersInit): Promise<VoidUploadResult>;
|
||||
/**
|
||||
* Enable/Disable encryption, file will be encrypted on the fly locally before uploading
|
||||
*/
|
||||
abstract setEncryption(s: boolean): void;
|
||||
|
||||
/**
|
||||
* Can we use local encryption
|
||||
*/
|
||||
abstract canEncrypt(): boolean;
|
||||
|
||||
/**
|
||||
* Enable/Disable encryption, file will be encrypted on the fly locally before uploading
|
||||
*/
|
||||
abstract setEncryption(s: boolean): void;
|
||||
|
||||
/**
|
||||
* Get the encryption key, should be called after enableEncryption()
|
||||
*/
|
||||
abstract getEncryptionKey(): string | undefined;
|
||||
}
|
||||
/**
|
||||
* Get the encryption key, should be called after enableEncryption()
|
||||
*/
|
||||
abstract getEncryptionKey(): string | undefined;
|
||||
}
|
||||
|
@ -2,99 +2,140 @@ import { UploadState, VoidUploader } from "./upload";
|
||||
import { VoidUploadResult } from "./index";
|
||||
|
||||
export class XHRUploader extends VoidUploader {
|
||||
canEncrypt(): boolean {
|
||||
return false;
|
||||
}
|
||||
canEncrypt(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
setEncryption() {
|
||||
//noop
|
||||
}
|
||||
setEncryption() {
|
||||
//noop
|
||||
}
|
||||
|
||||
getEncryptionKey() {
|
||||
return undefined;
|
||||
}
|
||||
getEncryptionKey() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async upload(headers?: HeadersInit): Promise<VoidUploadResult> {
|
||||
this.onStateChange?.(UploadState.Hashing);
|
||||
const hash = await this.digest(this.file);
|
||||
if (this.file.size > this.maxChunkSize) {
|
||||
return await this.#doSplitXHRUpload(hash, this.maxChunkSize, headers);
|
||||
} else {
|
||||
return await this.#xhrSegment(this.file, hash, undefined, undefined, 1, 1, headers);
|
||||
}
|
||||
async upload(headers?: HeadersInit): Promise<VoidUploadResult> {
|
||||
this.onStateChange?.(UploadState.Hashing);
|
||||
const hash = await this.digest(this.file);
|
||||
if (this.file.size > this.maxChunkSize) {
|
||||
return await this.#doSplitXHRUpload(hash, this.maxChunkSize, headers);
|
||||
} else {
|
||||
return await this.#xhrSegment(
|
||||
this.file,
|
||||
hash,
|
||||
undefined,
|
||||
undefined,
|
||||
1,
|
||||
1,
|
||||
headers
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async #doSplitXHRUpload(hash: string, splitSize: number, headers?: HeadersInit) {
|
||||
let xhr: VoidUploadResult | null = null;
|
||||
const segments = Math.ceil(this.file.size / splitSize);
|
||||
for (let s = 0; s < segments; s++) {
|
||||
const offset = s * splitSize;
|
||||
const slice = this.file.slice(offset, offset + splitSize, this.file.type);
|
||||
xhr = await this.#xhrSegment(slice, hash, xhr?.file?.id, xhr?.file?.metadata?.editSecret, s + 1, segments, headers);
|
||||
if (!xhr.ok) {
|
||||
break;
|
||||
async #doSplitXHRUpload(
|
||||
hash: string,
|
||||
splitSize: number,
|
||||
headers?: HeadersInit
|
||||
) {
|
||||
let xhr: VoidUploadResult | null = null;
|
||||
const segments = Math.ceil(this.file.size / splitSize);
|
||||
for (let s = 0; s < segments; s++) {
|
||||
const offset = s * splitSize;
|
||||
const slice = this.file.slice(offset, offset + splitSize, this.file.type);
|
||||
xhr = await this.#xhrSegment(
|
||||
slice,
|
||||
hash,
|
||||
xhr?.file?.id,
|
||||
xhr?.file?.metadata?.editSecret,
|
||||
s + 1,
|
||||
segments,
|
||||
headers
|
||||
);
|
||||
if (!xhr.ok) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return xhr!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a segment of the file
|
||||
* @param segment
|
||||
* @param fullDigest Full file hash
|
||||
* @param id
|
||||
* @param editSecret
|
||||
* @param part Segment number
|
||||
* @param partOf Total number of segments
|
||||
* @param headers
|
||||
*/
|
||||
async #xhrSegment(
|
||||
segment: ArrayBuffer | Blob,
|
||||
fullDigest: string,
|
||||
id?: string,
|
||||
editSecret?: string,
|
||||
part?: number,
|
||||
partOf?: number,
|
||||
headers?: HeadersInit
|
||||
) {
|
||||
this.onStateChange?.(UploadState.Uploading);
|
||||
|
||||
const absoluteUrl = id ? `${this.uri}/upload/${id}` : `${this.uri}/upload`;
|
||||
const authValue = this.auth
|
||||
? await this.auth(absoluteUrl, "POST")
|
||||
: undefined;
|
||||
|
||||
return await new Promise<VoidUploadResult>((resolve, reject) => {
|
||||
try {
|
||||
const req = new XMLHttpRequest();
|
||||
req.onreadystatechange = () => {
|
||||
if (req.readyState === XMLHttpRequest.DONE && req.status === 200) {
|
||||
const rsp = JSON.parse(req.responseText) as VoidUploadResult;
|
||||
resolve(rsp);
|
||||
} else if (
|
||||
req.readyState === XMLHttpRequest.DONE &&
|
||||
req.status === 403
|
||||
) {
|
||||
const contentType = req.getResponseHeader("content-type");
|
||||
if (contentType?.toLowerCase().trim().indexOf("text/html") === 0) {
|
||||
this.onProxyChallenge?.(req.response);
|
||||
this.onStateChange?.(UploadState.Challenge);
|
||||
reject(new Error("CF Challenge"));
|
||||
}
|
||||
}
|
||||
};
|
||||
req.upload.onprogress = (e) => {
|
||||
if (e instanceof ProgressEvent) {
|
||||
this.onProgress?.(e.loaded);
|
||||
}
|
||||
};
|
||||
req.open("POST", absoluteUrl);
|
||||
req.setRequestHeader("Content-Type", "application/octet-stream");
|
||||
req.setRequestHeader(
|
||||
"V-Content-Type",
|
||||
!this.file.type ? "application/octet-stream" : this.file.type
|
||||
);
|
||||
req.setRequestHeader(
|
||||
"V-Filename",
|
||||
"name" in this.file ? this.file.name : ""
|
||||
);
|
||||
req.setRequestHeader("V-Full-Digest", fullDigest);
|
||||
req.setRequestHeader("V-Segment", `${part}/${partOf}`);
|
||||
if (authValue) {
|
||||
req.withCredentials = true;
|
||||
req.setRequestHeader("Authorization", authValue);
|
||||
}
|
||||
return xhr!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a segment of the file
|
||||
* @param segment
|
||||
* @param fullDigest Full file hash
|
||||
* @param id
|
||||
* @param editSecret
|
||||
* @param part Segment number
|
||||
* @param partOf Total number of segments
|
||||
* @param headers
|
||||
*/
|
||||
async #xhrSegment(segment: ArrayBuffer | Blob, fullDigest: string,
|
||||
id?: string, editSecret?: string, part?: number, partOf?: number, headers?: HeadersInit) {
|
||||
this.onStateChange?.(UploadState.Uploading);
|
||||
|
||||
return await new Promise<VoidUploadResult>((resolve, reject) => {
|
||||
try {
|
||||
const req = new XMLHttpRequest();
|
||||
req.onreadystatechange = () => {
|
||||
if (req.readyState === XMLHttpRequest.DONE && req.status === 200) {
|
||||
const rsp = JSON.parse(req.responseText) as VoidUploadResult;
|
||||
resolve(rsp);
|
||||
} else if (req.readyState === XMLHttpRequest.DONE && req.status === 403) {
|
||||
const contentType = req.getResponseHeader("content-type");
|
||||
if (contentType?.toLowerCase().trim().indexOf("text/html") === 0) {
|
||||
this.onProxyChallenge?.(req.response);
|
||||
this.onStateChange?.(UploadState.Challenge);
|
||||
reject(new Error("CF Challenge"));
|
||||
}
|
||||
}
|
||||
};
|
||||
req.upload.onprogress = (e) => {
|
||||
if (e instanceof ProgressEvent) {
|
||||
this.onProgress?.(e.loaded);
|
||||
}
|
||||
};
|
||||
req.open("POST", id ? `${this.uri}/upload/${id}` : `${this.uri}/upload`);
|
||||
req.setRequestHeader("Content-Type", "application/octet-stream");
|
||||
req.setRequestHeader("V-Content-Type", !this.file.type ? "application/octet-stream" : this.file.type);
|
||||
req.setRequestHeader("V-Filename", "name" in this.file ? this.file.name : "");
|
||||
req.setRequestHeader("V-Full-Digest", fullDigest);
|
||||
req.setRequestHeader("V-Segment", `${part}/${partOf}`)
|
||||
if (this.auth) {
|
||||
req.withCredentials = true;
|
||||
req.setRequestHeader("Authorization", `Bearer ${this.auth}`);
|
||||
}
|
||||
if (editSecret) {
|
||||
req.setRequestHeader("V-EditSecret", editSecret);
|
||||
}
|
||||
if (headers) {
|
||||
for (const [k, v] of Object.entries(headers)) {
|
||||
req.setRequestHeader(k, v);
|
||||
}
|
||||
}
|
||||
req.send(segment);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (editSecret) {
|
||||
req.setRequestHeader("V-EditSecret", editSecret);
|
||||
}
|
||||
if (headers) {
|
||||
for (const [k, v] of Object.entries(headers)) {
|
||||
req.setRequestHeader(k, v);
|
||||
}
|
||||
}
|
||||
req.send(segment);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user