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