feat: add option to exclude images from export (#44)
- Add 'Include medication images' checkbox in export section - Default: enabled (full backup with images) - Disabled: much smaller export (~50 KB instead of several MB) - Helpful for quick backups or when importing to another instance
This commit is contained in:
@@ -233,11 +233,12 @@ export async function exportRoutes(app: FastifyInstance) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /export - Export all user data
|
||||
// ---------------------------------------------------------------------------
|
||||
app.get<{ Querystring: { includeSensitive?: string } }>(
|
||||
app.get<{ Querystring: { includeSensitive?: string; includeImages?: string } }>(
|
||||
"/export",
|
||||
async (request, reply) => {
|
||||
const userId = await getUserId(request, reply);
|
||||
const includeSensitive = request.query.includeSensitive === "true";
|
||||
const includeImages = request.query.includeImages !== "false"; // Default to true
|
||||
|
||||
// 1. Load all medications
|
||||
const meds = await db.select().from(medications).where(eq(medications.userId, userId)).orderBy(medications.id);
|
||||
@@ -264,7 +265,7 @@ export async function exportRoutes(app: FastifyInstance) {
|
||||
expiryDate: med.expiryDate,
|
||||
notes: med.notes,
|
||||
intakeRemindersEnabled: med.intakeRemindersEnabled ?? false,
|
||||
image: imageToBase64(med.imageUrl),
|
||||
image: includeImages ? imageToBase64(med.imageUrl) : null,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
+24
-11
@@ -366,6 +366,7 @@ function AppContent() {
|
||||
// Export/Import state
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [exportIncludeImages, setExportIncludeImages] = useState(true);
|
||||
// User dropdown state (for mobile click-based behavior)
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
|
||||
|
||||
@@ -1087,10 +1088,10 @@ function AppContent() {
|
||||
}
|
||||
|
||||
// Export data to JSON file
|
||||
async function handleExport() {
|
||||
async function handleExport(includeImages: boolean = true) {
|
||||
setExporting(true);
|
||||
try {
|
||||
const res = await fetch('/api/export?includeSensitive=true', {
|
||||
const res = await fetch(`/api/export?includeSensitive=true&includeImages=${includeImages}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error("Export failed");
|
||||
@@ -2847,20 +2848,32 @@ function AppContent() {
|
||||
<div className="setting-section">
|
||||
<div className="setting-group">
|
||||
{/* Export */}
|
||||
<div className="action-card">
|
||||
<div className="action-card" style={{flexDirection: 'column', alignItems: 'stretch'}}>
|
||||
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px'}}>
|
||||
<div className="action-card-content">
|
||||
<span className="action-card-title">{t('exportImport.exportTitle')}</span>
|
||||
<span className="action-card-desc">{t('exportImport.exportDesc')}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={handleExport}
|
||||
disabled={exporting}
|
||||
>
|
||||
{exporting ? t('exportImport.exporting') : t('exportImport.export')}
|
||||
</button>
|
||||
</div>
|
||||
<label className="toggle-label" style={{marginBottom: '12px'}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={exportIncludeImages}
|
||||
onChange={(e) => setExportIncludeImages(e.target.checked)}
|
||||
/>
|
||||
<span>{t('exportImport.includeImages')}</span>
|
||||
<span className="info-tooltip" data-tooltip={t('exportImport.includeImagesHint')}>ⓘ</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={() => handleExport(exportIncludeImages)}
|
||||
disabled={exporting}
|
||||
style={{alignSelf: 'flex-end'}}
|
||||
>
|
||||
{exporting ? t('exportImport.exporting') : t('exportImport.export')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Import */}
|
||||
<div className="action-card">
|
||||
|
||||
@@ -372,6 +372,8 @@
|
||||
"selectFile": "Datei auswählen",
|
||||
"includeSensitive": "Sensible Daten einschließen (Benachrichtigungs-URLs)",
|
||||
"sensitiveWarning": "Benachrichtigungs-URLs können Passwörter enthalten und werden im Klartext gespeichert.",
|
||||
"includeImages": "Medikamentenbilder einschließen",
|
||||
"includeImagesHint": "Bilder vergrößern die Datei erheblich. Deaktivieren für kleinere Exports (~50 KB statt mehrere MB).",
|
||||
"confirmImport": "Alle Daten ersetzen?",
|
||||
"confirmImportMessage": "Dies löscht dauerhaft alle deine aktuellen Medikamente, Einnahmehistorie, Einstellungen und Teilen-Links und ersetzt sie durch die importierten Daten.",
|
||||
"confirmImportWarning": "Diese Aktion kann nicht rückgängig gemacht werden!",
|
||||
|
||||
@@ -374,6 +374,8 @@
|
||||
"selectFile": "Select File",
|
||||
"includeSensitive": "Include sensitive data (notification URLs)",
|
||||
"sensitiveWarning": "Notification URLs may contain passwords and will be stored in plain text.",
|
||||
"includeImages": "Include medication images",
|
||||
"includeImagesHint": "Images significantly increase file size. Uncheck for smaller exports (~50 KB instead of several MB).",
|
||||
"confirmImport": "Replace All Data?",
|
||||
"confirmImportMessage": "This will permanently delete all your current medications, dose history, settings, and share links, then replace them with the imported data.",
|
||||
"confirmImportWarning": "This action cannot be undone!",
|
||||
|
||||
Reference in New Issue
Block a user