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:
Daniel Volz
2026-01-18 09:12:12 +01:00
committed by GitHub
parent 48ae48a165
commit fb0b3df794
4 changed files with 31 additions and 13 deletions
+3 -2
View File
@@ -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
View File
@@ -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">
+2
View File
@@ -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!",
+2
View File
@@ -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!",