Merge branch 'feat/onedrive-multiple-providers' into tmp/main

This commit is contained in:
David Bauch
2025-07-13 21:38:11 -04:00
159 changed files with 5935 additions and 5057 deletions

View File

@@ -1,9 +1,10 @@
import type { CreateFolderParams, DeleteFileParams, UpdateFileParams, UploadFileParams } from "@/lib/types";
import type { CreateFolderParams, DeleteFileParams, UpdateFileParams, UploadFileParams } from "@nimbus/shared";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import axios, { type AxiosError } from "axios";
import env from "@nimbus/env/client";
import { toast } from "sonner";
const API_BASE = `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/files`;
const BASE_FILE_URL = `${env.NEXT_PUBLIC_BACKEND_URL}/api/files`;
const defaultAxiosConfig = {
headers: {
"Content-Type": "application/json",
@@ -16,7 +17,7 @@ export function useGetFiles(parentId: string, pageSize: number, returnedValues:
return useQuery({
queryKey: ["files", parentId, nextPageToken, pageSize],
queryFn: async () => {
const response = await axios.get(API_BASE, {
const response = await axios.get(BASE_FILE_URL, {
params: { parentId, pageSize, returnedValues, pageToken: nextPageToken },
...defaultAxiosConfig,
});
@@ -31,7 +32,7 @@ export function useGetFile(fileId: string, returnedValues: string[]) {
return useQuery({
queryKey: ["file", fileId, returnedValues],
queryFn: async () => {
const response = await axios.get(`${API_BASE}/${fileId}`, {
const response = await axios.get(`${BASE_FILE_URL}/${fileId}`, {
params: { returnedValues },
...defaultAxiosConfig,
});
@@ -46,7 +47,7 @@ export function useDeleteFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ fileId }: DeleteFileParams) => {
const response = await axios.delete(API_BASE, {
const response = await axios.delete(BASE_FILE_URL, {
params: { fileId },
...defaultAxiosConfig,
});
@@ -67,7 +68,7 @@ export function useUpdateFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ fileId, ...dataToUpdate }: UpdateFileParams) => {
const response = await axios.put(`${API_BASE}`, dataToUpdate, {
const response = await axios.put(`${BASE_FILE_URL}`, dataToUpdate, {
params: { fileId },
...defaultAxiosConfig,
});
@@ -89,7 +90,7 @@ export function useCreateFolder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ name, parentId }: CreateFolderParams) => {
const response = await axios.post(API_BASE, null, {
const response = await axios.post(BASE_FILE_URL, null, {
...defaultAxiosConfig,
params: {
name,
@@ -115,19 +116,18 @@ export function useUploadFile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ file, parentId, onProgress, returnedValues }: UploadFileParams) => {
mutationFn: async ({ file, parentId, onProgress }: UploadFileParams) => {
// ? Maybe look into Tanstack Form for this implementation
const formData = new FormData();
formData.append("file", file);
const response = await axios.post(`${API_BASE}/upload`, formData, {
const response = await axios.post(`${BASE_FILE_URL}/upload`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
withCredentials: true,
params: {
parentId,
returnedValues,
},
onUploadProgress: progressEvent => {
if (onProgress && progressEvent.total) {
@@ -150,3 +150,103 @@ export function useUploadFile() {
}
export function useUploadFolder() {}
export function useDownloadFile() {
return useMutation({
mutationFn: async ({
fileId,
exportMimeType,
fileName,
onProgress,
}: {
fileId: string;
exportMimeType?: string;
fileName?: string;
onProgress?: (progress: number) => void;
}) => {
const params = new URLSearchParams();
if (exportMimeType) {
params.append("exportMimeType", exportMimeType);
}
const response = await fetch(`${BASE_FILE_URL}/download/${fileId}?${params.toString()}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
});
if (!response.ok) {
const errorData = (await response.json().catch(() => ({}))) as { message?: string };
const errorMessage = errorData?.message || "Failed to download file";
throw new Error(errorMessage);
}
// Get the filename from the Content-Disposition header
const contentDisposition = response.headers.get("Content-Disposition");
let filename = fileName || "download";
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1];
}
}
// Track download progress if the response supports it
const contentLength = response.headers.get("Content-Length");
const total = contentLength ? Number.parseInt(contentLength, 10) : 0;
let loaded = 0;
if (response.body && total > 0) {
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
// Update progress
if (onProgress && total > 0) {
const progress = Math.round((loaded / total) * 100);
onProgress(progress);
}
}
// Combine chunks into blob
const blob = new Blob(chunks);
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
// Fallback for responses without progress tracking
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
return { success: true, filename };
},
onSuccess: data => {
toast.success(`${data.filename} downloaded successfully`);
},
onError: error => {
console.error("Download error:", error);
toast.error(error instanceof Error ? error.message : "Failed to download file");
},
});
}