aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-12-08 10:35:17 +0000
committerGitHub <noreply@github.com>2025-12-08 10:35:17 +0000
commit20d3761c89d566cf28ef1a22db14ad4f6eef2f17 (patch)
tree3a665014c098603f5e631970afc66cb17586d054
parentb6c2dadd88540eb7181b1af157ea4577157763a5 (diff)
downloadkarakeep-20d3761c89d566cf28ef1a22db14ad4f6eef2f17.tar.zst
fix: check import quota before importing bookmarks (#2232)
* feat: check import quota before importing bookmarks Add quota validation before bookmark import to prevent users from exceeding their bookmark limits. The implementation includes: - New QuotaService.canImportBookmarks() method to check if user can import N bookmarks - New tRPC checkImportQuota procedure for client-side quota validation - Updated useBookmarkImport hook to parse files and check quota before import - Added error banner in ImportExport component to display quota errors - Optimized file parsing to avoid reading the file twice The quota check displays remaining bookmarks and provides clear error messages when the import would exceed the user's quota. * fix * some fixes --------- Co-authored-by: Claude <noreply@anthropic.com>
-rw-r--r--apps/web/components/settings/ImportExport.tsx13
-rw-r--r--apps/web/lib/hooks/useBookmarkImport.ts43
-rw-r--r--packages/shared/import-export/index.ts1
3 files changed, 54 insertions, 3 deletions
diff --git a/apps/web/components/settings/ImportExport.tsx b/apps/web/components/settings/ImportExport.tsx
index 7d127443..b6e4da9a 100644
--- a/apps/web/components/settings/ImportExport.tsx
+++ b/apps/web/components/settings/ImportExport.tsx
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect, useState } from "react";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button, buttonVariants } from "@/components/ui/button";
import FilePickerButton from "@/components/ui/file-picker-button";
import { Progress } from "@/components/ui/progress";
@@ -15,7 +16,7 @@ import { useBookmarkImport } from "@/lib/hooks/useBookmarkImport";
import { useTranslation } from "@/lib/i18n/client";
import { cn } from "@/lib/utils";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { Download, Loader2, Upload } from "lucide-react";
+import { AlertCircle, Download, Loader2, Upload } from "lucide-react";
import { Card, CardContent } from "../ui/card";
import { toast } from "../ui/use-toast";
@@ -131,10 +132,18 @@ function ExportButton() {
export function ImportExportRow() {
const { t } = useTranslation();
- const { importProgress, runUploadBookmarkFile } = useBookmarkImport();
+ const { importProgress, quotaError, runUploadBookmarkFile } =
+ useBookmarkImport();
return (
<div className="flex flex-col gap-3">
+ {quotaError && (
+ <Alert variant="destructive" className="relative">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>Import Quota Exceeded</AlertTitle>
+ <AlertDescription>{quotaError}</AlertDescription>
+ </Alert>
+ )}
<div className="grid gap-4 md:grid-cols-2">
<ImportCard
text="HTML File"
diff --git a/apps/web/lib/hooks/useBookmarkImport.ts b/apps/web/lib/hooks/useBookmarkImport.ts
index d4ffda4e..0d9bbaaf 100644
--- a/apps/web/lib/hooks/useBookmarkImport.ts
+++ b/apps/web/lib/hooks/useBookmarkImport.ts
@@ -13,10 +13,12 @@ import {
useAddBookmarkToList,
useCreateBookmarkList,
} from "@karakeep/shared-react/hooks/lists";
+import { api } from "@karakeep/shared-react/trpc";
import {
importBookmarksFromFile,
ImportSource,
ParsedBookmark,
+ parseImportFile,
} from "@karakeep/shared/import-export";
import {
BookmarkTypes,
@@ -36,7 +38,9 @@ export function useBookmarkImport() {
const [importProgress, setImportProgress] = useState<ImportProgress | null>(
null,
);
+ const [quotaError, setQuotaError] = useState<string | null>(null);
+ const apiUtils = api.useUtils();
const { mutateAsync: createImportSession } = useCreateImportSession();
const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook();
const { mutateAsync: createList } = useCreateBookmarkList();
@@ -51,6 +55,36 @@ export function useBookmarkImport() {
file: File;
source: ImportSource;
}) => {
+ // Clear any previous quota error
+ setQuotaError(null);
+
+ // First, parse the file to count bookmarks
+ const textContent = await file.text();
+ const parsedBookmarks = parseImportFile(source, textContent);
+ const bookmarkCount = parsedBookmarks.length;
+
+ // Check quota before proceeding
+ if (bookmarkCount > 0) {
+ const quotaUsage =
+ await apiUtils.client.subscriptions.getQuotaUsage.query();
+
+ if (
+ !quotaUsage.bookmarks.unlimited &&
+ quotaUsage.bookmarks.quota !== null
+ ) {
+ const remaining =
+ quotaUsage.bookmarks.quota - quotaUsage.bookmarks.used;
+
+ if (remaining < bookmarkCount) {
+ const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? "" : "s"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;
+ setQuotaError(errorMsg);
+ throw new Error(errorMsg);
+ }
+ }
+ }
+
+ // Proceed with import if quota check passes
+ // Use a custom parser to avoid re-parsing the file
const result = await importBookmarksFromFile(
{
file,
@@ -122,7 +156,12 @@ export function useBookmarkImport() {
},
onProgress: (done, total) => setImportProgress({ done, total }),
},
- {},
+ {
+ // Use a custom parser to avoid re-parsing the file
+ parsers: {
+ [source]: () => parsedBookmarks,
+ },
+ },
);
return result;
},
@@ -158,6 +197,8 @@ export function useBookmarkImport() {
return {
importProgress,
+ quotaError,
+ clearQuotaError: () => setQuotaError(null),
runUploadBookmarkFile: uploadBookmarkFileMutation.mutateAsync,
isImporting: uploadBookmarkFileMutation.isPending,
};
diff --git a/packages/shared/import-export/index.ts b/packages/shared/import-export/index.ts
index 2d720d0b..dd266e06 100644
--- a/packages/shared/import-export/index.ts
+++ b/packages/shared/import-export/index.ts
@@ -1,3 +1,4 @@
export * from "./exporters";
export * from "./importer";
export type { ImportSource, ParsedBookmark } from "./parsers";
+export { parseImportFile } from "./parsers";