diff --git a/app/components/general-layout.tsx b/app/components/general-layout.tsx
index c0b079b..add18d6 100644
--- a/app/components/general-layout.tsx
+++ b/app/components/general-layout.tsx
@@ -108,8 +108,8 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
logout()}>
- Log Out
-
+ Logout
+
diff --git a/app/components/lib/sdk-context.tsx b/app/components/lib/sdk-context.tsx
new file mode 100644
index 0000000..a2d455e
--- /dev/null
+++ b/app/components/lib/sdk-context.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import {Sdk} from "@lumeweb/portal-sdk";
+
+export const SdkContext = React.createContext<
+ Partial
+>({});
+
+export const SdkContextProvider: React.FC< {sdk: Sdk, children: React.ReactNode}> = ({sdk, children}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export function useSdk(): Partial{
+ return React.useContext(SdkContext);
+}
diff --git a/app/components/lib/uppy-file-upload.ts b/app/components/lib/uppy-file-upload.ts
new file mode 100644
index 0000000..a6224ec
--- /dev/null
+++ b/app/components/lib/uppy-file-upload.ts
@@ -0,0 +1,63 @@
+import Uppy, {BasePlugin, DefaultPluginOptions} from '@uppy/core';
+import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
+import {S5Client} from "@lumeweb/s5-js";
+import {AxiosProgressEvent} from "axios";
+
+
+interface UppyFileUploadOptions extends DefaultPluginOptions {
+ sdk: Sdk;
+}
+
+export default class UppyFileUpload extends BasePlugin {
+ private _sdk: Sdk;
+
+ constructor(uppy: Uppy, opts?: UppyFileUploadOptions) {
+ super(uppy, opts);
+ this.id = opts?.id || 'file-upload';
+ this.type = 'uploader';
+ this._sdk = opts?.sdk as Sdk;
+ }
+
+ install() {
+ this.uppy.addUploader(this.handleUpload.bind(this));
+ }
+
+ private async handleUpload(fileIDs: string[]) {
+ for (const fileID of fileIDs) {
+ const file = this.uppy.getFile(fileID);
+ if (!file) {
+ continue;
+ }
+
+ // @ts-ignore
+ if (file.uploader !== 'file') {
+ continue;
+ }
+
+ const uploadLimit = await this._sdk.account().uploadLimit();
+
+ let data = file.data;
+
+ if (file.data instanceof Blob) {
+ data = new File([data], file.name, {type: file.type});
+ }
+
+ try {
+ await this._sdk.protocols().get(PROTOCOL_S5).getSdk().uploadFile(data as File, {
+ largeFileSize: uploadLimit,
+ onUploadProgress: (progressEvent: AxiosProgressEvent) => {
+ this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
+ uploader: this,
+ bytesUploaded: progressEvent.loaded,
+ bytesTotal: progressEvent.total,
+ })
+ }
+ });
+
+ this.uppy.emit('upload-success', file, {uploadURL: null});
+ } catch (err) {
+ this.uppy.emit('upload-error', file, err);
+ }
+ }
+ }
+}
diff --git a/app/components/lib/uppy.ts b/app/components/lib/uppy.ts
index 6b725f8..3dc123c 100644
--- a/app/components/lib/uppy.ts
+++ b/app/components/lib/uppy.ts
@@ -1,17 +1,14 @@
-import Uppy, { type State, debugLogger } from "@uppy/core"
+import Uppy, {debugLogger, type State, UppyFile} from "@uppy/core"
import Tus from "@uppy/tus"
import toArray from "@uppy/utils/lib/toArray"
-import {
- type ChangeEvent,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState
-} from "react"
-import DropTarget, { type DropTargetOptions } from "./uppy-dropzone"
+import {type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState} from "react"
+import DropTarget, {type DropTargetOptions} from "./uppy-dropzone"
+import {useSdk} from "~/components/lib/sdk-context.js";
+import UppyFileUpload from "~/components/lib/uppy-file-upload.js";
+import {PROTOCOL_S5, Sdk} from "@lumeweb/portal-sdk";
+import {S5Client} from "@lumeweb/s5-js";
const LISTENING_EVENTS = [
"upload",
@@ -23,12 +20,26 @@ const LISTENING_EVENTS = [
] as const
export function useUppy({
- uploader,
endpoint
}: {
- uploader: "tus"
endpoint: string
}) {
+ const sdk = useSdk()
+
+ const [uploadLimit, setUploadLimit] = useState(0)
+
+ useEffect(() => {
+ async function getUploadLimit() {
+ try {
+ const limit = await sdk.account!().uploadLimit();
+ setUploadLimit(limit);
+ } catch (err) {
+ console.log('Error occured while fetching upload limit', err);
+ }
+ }
+ getUploadLimit();
+ }, []);
+
const inputRef = useRef(null)
const [targetRef, _setTargetRef] = useState(null)
const uppyInstance = useRef()
@@ -82,7 +93,29 @@ export function useUppy({
useEffect(() => {
if (!targetRef) return
- const uppy = new Uppy({ logger: debugLogger }).use(DropTarget, {
+ const tusPreprocessor = async (fileIDs: string[]) => {
+ for(const fileID of fileIDs) {
+ const file = uppyInstance.current?.getFile(fileID) as UppyFile
+ // @ts-ignore
+ if (file.uploader === "tus") {
+ uppyInstance.current?.setFileState(fileID, {
+ tus: await sdk.protocols!().get(PROTOCOL_S5).getSdk().getTusOptions(file.data as File)
+ })
+ }
+ }
+ }
+
+ const uppy = new Uppy({
+ logger: debugLogger,
+ onBeforeUpload: (files) => {
+ for (const file of Object.entries(files)) {
+ // @ts-ignore
+ file[1].uploader = file[1].size > uploadLimit ? "tus" : "file";
+ }
+
+ return true;
+ },
+ }).use(DropTarget, {
target: targetRef
} as DropTargetOptions)
@@ -97,6 +130,25 @@ export function useUppy({
uppyInstance.current?.addFiles(files)
}
+ uppy.iteratePlugins((plugin) => {
+ uppy.removePlugin(plugin);
+ });
+
+ uppy.use(UppyFileUpload, { sdk: sdk as Sdk })
+
+ let useTus = false;
+
+ uppyInstance.current?.getFiles().forEach((file) => {
+ if (file.size > uploadLimit) {
+ useTus = true;
+ }
+ })
+
+ if (useTus) {
+ uppy.use(Tus, { endpoint: endpoint, limit: 6 })
+ uppy.addPreProcessor(tusPreprocessor)
+ }
+
// We clear the input after a file is selected, because otherwise
// change event is not fired in Chrome and Safari when a file
// with the same name is selected.
@@ -109,12 +161,7 @@ export function useUppy({
}
})
- switch (uploader) {
- case "tus":
- uppy.use(Tus, { endpoint: endpoint, limit: 6 })
- break
- default:
- }
+
uppy.on("complete", (result) => {
if (result.failed.length === 0) {
@@ -148,7 +195,7 @@ export function useUppy({
})
}
setState("idle")
- }, [targetRef, endpoint, uploader])
+ }, [targetRef, endpoint, uploadLimit])
useEffect(() => {
return () => {
diff --git a/app/data/auth-provider.ts b/app/data/auth-provider.ts
index e29e9fe..55188b6 100644
--- a/app/data/auth-provider.ts
+++ b/app/data/auth-provider.ts
@@ -33,10 +33,8 @@ export type Identity = {
}
export class PortalAuthProvider implements RequiredAuthProvider {
- private sdk: Sdk;
-
constructor(apiUrl: string) {
- this.sdk = Sdk.create(apiUrl);
+ this._sdk = Sdk.create(apiUrl);
const methods: Array = [
'login',
@@ -55,9 +53,18 @@ export class PortalAuthProvider implements RequiredAuthProvider {
});
}
+ private _sdk: Sdk;
+
+ get sdk(): Sdk {
+ return this._sdk;
+ }
+
+ public static create(apiUrl: string): AuthProvider {
+ return new PortalAuthProvider(apiUrl);
+ }
+
async login(params: AuthFormRequest): Promise {
- const cookies = new Cookies();
- const ret = await this.sdk.account().login({
+ const ret = await this._sdk.account().login({
email: params.email,
password: params.password,
})
@@ -65,11 +72,11 @@ export class PortalAuthProvider implements RequiredAuthProvider {
let redirectTo: string | undefined;
if (ret) {
- cookies.set('jwt', this.sdk.account().jwtToken, {path: '/'});
redirectTo = params.redirectTo;
if (!redirectTo) {
redirectTo = ret ? "/dashboard" : "/login";
}
+ this._sdk.setAuthToken(this._sdk.account().jwtToken);
}
return {
@@ -79,38 +86,25 @@ export class PortalAuthProvider implements RequiredAuthProvider {
}
async logout(params: any): Promise {
- let ret = await this.sdk.account().logout();
- if (ret) {
- const cookies = new Cookies();
- cookies.remove('jwt');
- }
+ let ret = await this._sdk.account().logout();
return {success: ret, redirectTo: "/login"};
}
async check(params?: any): Promise {
- const cookies = new Cookies();
+ this.maybeSetupAuth();
- const jwtCookie = cookies.get('jwt');
-
- if (jwtCookie) {
- this.sdk.setAuthToken(jwtCookie);
- }
-
- const ret = await this.sdk.account().ping();
-
- if (!ret) {
- cookies.remove('jwt');
- }
+ const ret = await this._sdk.account().ping();
return {authenticated: ret, redirectTo: ret ? undefined : "/login"};
}
async onError(error: any): Promise {
+ const cookies = new Cookies();
return {logout: true};
}
async register(params: RegisterFormRequest): Promise {
- const ret = await this.sdk.account().register({
+ const ret = await this._sdk.account().register({
email: params.email,
password: params.password,
first_name: params.firstName,
@@ -132,7 +126,8 @@ export class PortalAuthProvider implements RequiredAuthProvider {
}
async getIdentity(params?: Identity): Promise {
- const ret = await this.sdk.account().info();
+ this.maybeSetupAuth();
+ const ret = await this._sdk.account().info();
if (!ret) {
return {identity: null};
@@ -148,12 +143,16 @@ export class PortalAuthProvider implements RequiredAuthProvider {
};
}
- public static create(apiUrl: string): AuthProvider {
- return new PortalAuthProvider(apiUrl);
+ maybeSetupAuth(): void {
+ const cookies = new Cookies();
+ const jwtCookie = cookies.get('auth_token');
+ if (jwtCookie) {
+ this._sdk.setAuthToken(jwtCookie);
+ }
}
}
-interface RequiredAuthProvider extends AuthProvider {
+export interface RequiredAuthProvider extends AuthProvider {
login: AuthProvider['login'];
logout: AuthProvider['logout'];
check: AuthProvider['check'];
diff --git a/app/data/pinning.ts b/app/data/pinning.ts
new file mode 100644
index 0000000..80d9540
--- /dev/null
+++ b/app/data/pinning.ts
@@ -0,0 +1,55 @@
+interface PinningStatus {
+ id: string;
+ progress: number;
+ status: 'inprogress' | 'completed' | 'stale';
+}
+
+// biome-ignore lint/complexity/noStaticOnlyClass:
+class PinningProcess {
+ private static instances: Map = new Map();
+
+ static async pin(id: string): Promise<{ success: boolean; message: string }> {
+ if (PinningProcess.instances.has(id)) {
+ return { success: false, message: "ID is already being processed" };
+ }
+
+ const pinningStatus: PinningStatus = { id, progress: 0, status: 'inprogress' };
+ PinningProcess.instances.set(id, pinningStatus);
+
+ // Simulate async progress
+ (async () => {
+ for (let progress = 1; progress <= 100; progress++) {
+ await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * (500 - 100 + 1)) + 100)); // Simulate time passing with random duration between 100 and 500
+ pinningStatus.progress = progress;
+ if (progress === 100) {
+ pinningStatus.status = 'completed';
+ }
+ }
+ })();
+
+ return { success: true, message: "Pinning process started" };
+ }
+
+ static *pollProgress(id: string): Generator {
+ let status = PinningProcess.instances.get(id);
+ while (status && status.status !== 'completed') {
+ yield status;
+ status = PinningProcess.instances.get(id);
+ }
+ yield status ?? null; // Yield the final status, could be null if ID doesn't exist
+ }
+}
+
+// Example usage:
+// (async () => {
+// const { success, message } = await PinningProcess.pin("123");
+// console.log(message);
+// if (success) {
+// const progressGenerator = PinningProcess.pollProgress("123");
+// let result = progressGenerator.next();
+// while (!result.done) {
+// console.log(result.value); // Log the progress
+// result = progressGenerator.next();
+// }
+// }
+// })();
diff --git a/app/root.tsx b/app/root.tsx
index d4286cf..96d1531 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -15,6 +15,7 @@ import {Refine} from "@refinedev/core";
import {PortalAuthProvider} from "~/data/auth-provider.js";
import routerProvider from "@refinedev/remix-router";
import { defaultProvider } from "./data/file-provider";
+import {SdkContextProvider} from "~/components/lib/sdk-context.js";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
@@ -39,9 +40,10 @@ export function Layout({children}: { children: React.ReactNode }) {
}
export default function App() {
+ const auth = PortalAuthProvider.create("https://alpha.pinner.xyz")
return (
-
+
+
+
);
}
diff --git a/app/routes/account.tsx b/app/routes/account.tsx
index b8d32f7..41a4717 100644
--- a/app/routes/account.tsx
+++ b/app/routes/account.tsx
@@ -1,12 +1,23 @@
import { getFormProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
-import { BaseKey, useGetIdentity, useUpdate, useUpdatePassword } from "@refinedev/core";
+import {
+ BaseKey,
+ useGetIdentity,
+ useUpdate,
+ useUpdatePassword,
+} from "@refinedev/core";
import { useState } from "react";
import { z } from "zod";
import { Field } from "~/components/forms";
import { GeneralLayout } from "~/components/general-layout";
import { AddIcon, CloudIcon, CrownIcon } from "~/components/icons";
-import { ManagementCard, ManagementCardAvatar, ManagementCardContent, ManagementCardFooter, ManagementCardTitle } from "~/components/management-card";
+import {
+ ManagementCard,
+ ManagementCardAvatar,
+ ManagementCardContent,
+ ManagementCardFooter,
+ ManagementCardTitle,
+} from "~/components/management-card";
import { Button } from "~/components/ui/button";
import {
Dialog,
@@ -51,10 +62,12 @@ export default function MyAccount() {
Email Address
- {identity?.email}
+ {identity?.email}
-