From: PhiTux Date: Sat, 16 Aug 2025 15:58:55 +0000 (+0200) Subject: added feature to export data X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=4da931d2a4ee887015535b3bf783553dbf38f9bc;p=DailyTxT.git added feature to export data --- diff --git a/backend/go.mod b/backend/go.mod index 9724864..36168cc 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,11 +4,11 @@ go 1.24 require ( github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/google/uuid v1.6.0 golang.org/x/crypto v0.19.0 ) require ( - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect golang.org/x/sys v0.17.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 450ba80..4c947f8 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,9 +1,9 @@ github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= +github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= diff --git a/backend/handlers/additional.go b/backend/handlers/additional.go index 31dfcc6..9be842f 100644 --- a/backend/handlers/additional.go +++ b/backend/handlers/additional.go @@ -1,8 +1,10 @@ package handlers import ( + "archive/zip" "encoding/json" "fmt" + htmlpkg "html" "io" "net/http" "os" @@ -11,7 +13,12 @@ import ( "sort" "strconv" "strings" + "time" + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/ast" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" "github.com/phitux/dailytxt/backend/utils" ) @@ -1504,3 +1511,1191 @@ func Search(w http.ResponseWriter, r *http.Request) { // Return results utils.JSONResponse(w, http.StatusOK, results) } + +// LogEntry represents a single diary entry for export +type LogEntry struct { + Year int + Month int + Day int + Text string + DateWritten string + Files []string + Tags []int +} + +// ExportData handles exporting user data +func ExportData(w http.ResponseWriter, r *http.Request) { + // Get user ID and derived key from context + userID, ok := r.Context().Value(utils.UserIDKey).(int) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Get parameters from URL + period := r.URL.Query().Get("period") + if period == "" { + http.Error(w, "Missing period parameter", http.StatusBadRequest) + return + } else if period != "periodAll" && period != "periodVariable" { + http.Error(w, "Invalid period parameter", http.StatusBadRequest) + return + } + + var startYear, startMonth, startDay, endYear, endMonth, endDay int + var err error + + if period == "periodVariable" { + startDate := r.URL.Query().Get("startDate") + if startDate != "" { + startParts := strings.Split(startDate, "-") + if len(startParts) != 3 { + http.Error(w, "Invalid startDate format", http.StatusBadRequest) + return + } + startYear, _ = strconv.Atoi(startParts[0]) + startMonth, _ = strconv.Atoi(startParts[1]) + startDay, _ = strconv.Atoi(startParts[2]) + } else { + http.Error(w, "Missing startDate parameter", http.StatusBadRequest) + return + } + + endDate := r.URL.Query().Get("endDate") + if endDate != "" { + endParts := strings.Split(endDate, "-") + if len(endParts) != 3 { + http.Error(w, "Invalid endDate format", http.StatusBadRequest) + return + } + endYear, _ = strconv.Atoi(endParts[0]) + endMonth, _ = strconv.Atoi(endParts[1]) + endDay, _ = strconv.Atoi(endParts[2]) + } else { + http.Error(w, "Missing endDate parameter", http.StatusBadRequest) + return + } + } + + imagesInHTML := r.URL.Query().Get("imagesInHTML") == "true" + + split := r.URL.Query().Get("split") + if split == "" { + http.Error(w, "Missing split parameter", http.StatusBadRequest) + return + } else if split != "month" && split != "year" && split != "aio" { + http.Error(w, "Invalid split parameter", http.StatusBadRequest) + return + } + + tagsInHTML := r.URL.Query().Get("tagsInHTML") == "true" + + // Get encryption key + encKey, err := utils.GetEncryptionKey(userID, derivedKey) + if err != nil { + http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError) + return + } + + // Set response headers for ZIP download + var filename string + if period == "periodAll" { + filename = fmt.Sprintf("diary_export_all_%s.zip", time.Now().Format("2006-01-02")) + } else { + filename = fmt.Sprintf("diary_export_%d-%02d-%02d_to_%d-%02d-%02d.zip", + startYear, startMonth, startDay, + endYear, endMonth, endDay) + } + + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + + // Create ZIP writer + zipWriter := zip.NewWriter(w) + defer zipWriter.Close() + + // Collect all log entries for HTML generation + var allEntries []LogEntry + var yearlyEntries map[int][]LogEntry = make(map[int][]LogEntry) + var monthlyEntries map[string][]LogEntry = make(map[string][]LogEntry) + + // Helper function to check if a date is within range + isDateInRange := func(year, month, day int) bool { + if period == "periodAll" { + return true + } + + // Create time objects for comparison + targetDate := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) + startDateObj := time.Date(startYear, time.Month(startMonth), startDay, 0, 0, 0, 0, time.UTC) + endDateObj := time.Date(endYear, time.Month(endMonth), endDay, 0, 0, 0, 0, time.UTC) + + return !targetDate.Before(startDateObj) && !targetDate.After(endDateObj) + } + + // Determine year range to scan + var minYear, maxYear int + if period == "periodAll" { + // For "periodAll", scan a reasonable range + minYear = 2010 + maxYear = time.Now().Year() + 1 + } else { + minYear = startYear + maxYear = endYear + } + + // Process each year in the range + for year := minYear; year <= maxYear; year++ { + startMonthLoop := 1 + endMonthLoop := 12 + + // Optimize month range for specific date ranges + if period == "periodVariable" { + if year == startYear { + startMonthLoop = startMonth + } + if year == endYear { + endMonthLoop = endMonth + } + } + + for month := startMonthLoop; month <= endMonthLoop; month++ { + // Get month data + content, err := utils.GetMonth(userID, year, month) + if err != nil { + continue // Skip months that don't exist + } + + days, ok := content["days"].([]any) + if !ok { + continue + } + + // Process each day in the month + for _, dayInterface := range days { + day, ok := dayInterface.(map[string]any) + if !ok { + continue + } + + dayNum, ok := day["day"].(float64) + if !ok { + continue + } + + dayInt := int(dayNum) + + // Check if this specific day is within the date range + if !isDateInRange(year, month, dayInt) { + continue + } + + entry := LogEntry{ + Year: year, + Month: month, + Day: dayInt, + } + + // Decrypt text and date_written + if text, ok := day["text"].(string); ok && text != "" { + decryptedText, err := utils.DecryptText(text, encKey) + if err != nil { + utils.Logger.Printf("Error decrypting text for %d-%d-%d: %v", year, month, dayInt, err) + continue + } + entry.Text = decryptedText + + if dateWritten, ok := day["date_written"].(string); ok && dateWritten != "" { + decryptedDate, err := utils.DecryptText(dateWritten, encKey) + if err == nil { + entry.DateWritten = decryptedDate + } + } + } + + // Process files + if filesList, ok := day["files"].([]any); ok && len(filesList) > 0 { + for _, fileInterface := range filesList { + file, ok := fileInterface.(map[string]any) + if !ok { + continue + } + + fileID, ok := file["uuid_filename"].(string) + if !ok { + continue + } + + encFilename, ok := file["enc_filename"].(string) + if !ok { + continue + } + + // Decrypt filename + decryptedFilename, err := utils.DecryptText(encFilename, encKey) + if err != nil { + utils.Logger.Printf("Error decrypting filename %s: %v", fileID, err) + continue + } + + // Read and decrypt file content + fileContent, err := utils.ReadFile(userID, fileID) + if err != nil { + utils.Logger.Printf("Error reading file %s: %v", fileID, err) + continue + } + + decryptedContent, err := utils.DecryptFile(fileContent, encKey) + if err != nil { + utils.Logger.Printf("Error decrypting file %s: %v", fileID, err) + continue + } + + // Add file to ZIP + filePath := fmt.Sprintf("files/%d-%02d-%02d/%s", year, month, dayInt, decryptedFilename) + fileWriter, err := zipWriter.Create(filePath) + if err != nil { + utils.Logger.Printf("Error creating file in ZIP %s: %v", filePath, err) + continue + } + + _, err = fileWriter.Write(decryptedContent) + if err != nil { + utils.Logger.Printf("Error writing file to ZIP %s: %v", filePath, err) + continue + } + + entry.Files = append(entry.Files, decryptedFilename) + } + } + + // Add tags + if tags, ok := day["tags"].([]any); ok && len(tags) > 0 { + for _, tag := range tags { + if tagID, ok := tag.(float64); ok { + entry.Tags = append(entry.Tags, int(tagID)) + } + } + } + + // Add entry if it has content + if entry.Text != "" || len(entry.Files) > 0 || len(entry.Tags) > 0 { + allEntries = append(allEntries, entry) + + // Add to yearly collections + yearlyEntries[year] = append(yearlyEntries[year], entry) + + // Add to monthly collections + monthKey := fmt.Sprintf("%d-%02d", year, month) + monthlyEntries[monthKey] = append(monthlyEntries[monthKey], entry) + } + } + } + } + + // Create HTML files based on split preference + switch split { + case "month": + // Create one HTML per month + for monthKey, entries := range monthlyEntries { + if len(entries) > 0 { + htmlBytes, err := generateHTML(entries, userID, derivedKey, tagsInHTML, imagesInHTML) + if err != nil { + utils.Logger.Printf("Error generating HTML for month %s: %v", monthKey, err) + } else { + fileName := fmt.Sprintf("diary_%s.html", monthKey) + htmlWriter, err := zipWriter.Create(fileName) + if err != nil { + utils.Logger.Printf("Error creating month HTML in ZIP %s: %v", fileName, err) + } else { + _, err = htmlWriter.Write(htmlBytes) + if err != nil { + utils.Logger.Printf("Error writing month HTML to ZIP %s: %v", fileName, err) + } + } + } + } + } + + case "year": + // Create one HTML per year + for year, entries := range yearlyEntries { + if len(entries) > 0 { + htmlBytes, err := generateHTML(entries, userID, derivedKey, tagsInHTML, imagesInHTML) + if err != nil { + utils.Logger.Printf("Error generating HTML for year %d: %v", year, err) + } else { + fileName := fmt.Sprintf("diary_%d.html", year) + htmlWriter, err := zipWriter.Create(fileName) + if err != nil { + utils.Logger.Printf("Error creating year HTML in ZIP %s: %v", fileName, err) + } else { + _, err = htmlWriter.Write(htmlBytes) + if err != nil { + utils.Logger.Printf("Error writing year HTML to ZIP %s: %v", fileName, err) + } + } + } + } + } + + case "aio": + // Create one single HTML with all entries + if len(allEntries) > 0 { + htmlBytes, err := generateHTML(allEntries, userID, derivedKey, tagsInHTML, imagesInHTML) + if err != nil { + utils.Logger.Printf("Error generating HTML: %v", err) + } else { + // Add HTML to ZIP + htmlWriter, err := zipWriter.Create("diary_export.html") + if err != nil { + utils.Logger.Printf("Error creating HTML in ZIP: %v", err) + } else { + _, err = htmlWriter.Write(htmlBytes) + if err != nil { + utils.Logger.Printf("Error writing HTML to ZIP: %v", err) + } + } + } + } + } +} + +// generateHTML creates an HTML document with all diary entries +func generateHTML(entries []LogEntry, userID int, derivedKey string, includeTags bool, includeImages bool) ([]byte, error) { + // Load and decrypt tags if needed + var tagMap map[int]Tag + if includeTags { + var err error + tagMap, err = loadAndDecryptTags(userID, derivedKey) + if err != nil { + utils.Logger.Printf("Warning: Could not load tags for HTML export: %v", err) + tagMap = make(map[int]Tag) // Use empty map if loading fails + } + } + + // Sort entries by date (year, month, day) + sort.Slice(entries, func(i, j int) bool { + if entries[i].Year != entries[j].Year { + return entries[i].Year < entries[j].Year + } + if entries[i].Month != entries[j].Month { + return entries[i].Month < entries[j].Month + } + return entries[i].Day < entries[j].Day + }) + + var html strings.Builder + + // HTML header with embedded CSS + html.WriteString(` + + + + + DailyTxT Export + + + +`) + + // Header + html.WriteString(`
+

DailyTxT Export

`) + html.WriteString(fmt.Sprintf(`

Benutzer ID: %d

`, userID)) + html.WriteString(fmt.Sprintf(`

Exportiert am: %s

`, time.Now().Format("02.01.2006 15:04:05"))) + html.WriteString(fmt.Sprintf(`

Anzahl Einträge: %d

`, len(entries))) + html.WriteString(`
+`) + + // Process entries + for _, entry := range entries { + html.WriteString(`
+`) + + // Date header with weekday + date := time.Date(entry.Year, time.Month(entry.Month), entry.Day, 0, 0, 0, 0, time.UTC) + weekdays := []string{"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"} + weekday := weekdays[date.Weekday()] + dateStr := fmt.Sprintf("%s, %02d.%02d.%d", weekday, entry.Day, entry.Month, entry.Year) + html.WriteString(fmt.Sprintf(` +`, htmlpkg.EscapeString(dateStr))) + + html.WriteString(`
+`) + + // Entry text + if entry.Text != "" { + // Decode HTML entities and render markdown + text := htmlpkg.UnescapeString(entry.Text) + text = renderMarkdownToHTML(text) + html.WriteString(fmt.Sprintf(`
%s
+`, text)) + } + + // Images (if enabled and images exist) + if includeImages && len(entry.Files) > 0 { + imageFiles := make([]string, 0) + for _, file := range entry.Files { + // Check if file is an image (simple check by extension) + ext := strings.ToLower(filepath.Ext(file)) + if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".webp" { + imageFiles = append(imageFiles, file) + } + } + + if len(imageFiles) > 0 { + html.WriteString(`
+

Bilder

+ +
+`) + } + } + + // Tags + if includeTags && len(entry.Tags) > 0 { + html.WriteString(` +`) + } + + // Files + if len(entry.Files) > 0 { + html.WriteString(`
+

Dateien

+
    +`) + for _, file := range entry.Files { + filePath := fmt.Sprintf("files/%d-%02d-%02d/%s", entry.Year, entry.Month, entry.Day, file) + html.WriteString(fmt.Sprintf(`
  • %s
  • +`, htmlpkg.EscapeString(filePath), htmlpkg.EscapeString(file))) + } + html.WriteString(`
+
+`) + } + + html.WriteString(`
+
+`) + } + + html.WriteString(` + + + + + + + +`) + + return []byte(html.String()), nil +} + +// Tag represents a decrypted tag +type Tag struct { + ID int `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + Color string `json:"color"` +} + +// loadAndDecryptTags loads and decrypts user tags +func loadAndDecryptTags(userID int, derivedKey string) (map[int]Tag, error) { + content, err := utils.GetTags(userID) + if err != nil { + return nil, err + } + + // Get encryption key + encKey, err := utils.GetEncryptionKey(userID, derivedKey) + if err != nil { + return nil, err + } + + tagMap := make(map[int]Tag) + + if tags, ok := content["tags"].([]any); ok { + for _, tagInterface := range tags { + if tagData, ok := tagInterface.(map[string]any); ok { + if tagID, ok := tagData["id"].(float64); ok { + tag := Tag{ID: int(tagID)} + + // Decrypt name + if encName, ok := tagData["name"].(string); ok { + if decryptedName, err := utils.DecryptText(encName, encKey); err == nil { + tag.Name = decryptedName + } + } + + // Decrypt icon + if encIcon, ok := tagData["icon"].(string); ok { + if decryptedIcon, err := utils.DecryptText(encIcon, encKey); err == nil { + tag.Icon = decryptedIcon + } + } + + // Decrypt color + if encColor, ok := tagData["color"].(string); ok { + if decryptedColor, err := utils.DecryptText(encColor, encKey); err == nil { + tag.Color = decryptedColor + } + } + + tagMap[int(tagID)] = tag + } + } + } + } + + return tagMap, nil +} + +// renderMarkdownToHTML converts markdown to HTML using gomarkdown library +func renderMarkdownToHTML(text string) string { + // Create parser with extensions including hard line breaks + extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.HardLineBreak + p := parser.NewWithExtensions(extensions) + + // Create HTML renderer with options for Prism.js compatibility + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{ + Flags: htmlFlags, + RenderNodeHook: func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { + if codeBlock, ok := node.(*ast.CodeBlock); ok && entering { + lang := string(codeBlock.Info) + if lang == "" { + lang = "none" + } + + // Write opening tag with Prism.js class + fmt.Fprintf(w, `
`, lang)
+
+				// Write escaped content
+				content := string(codeBlock.Literal)
+				html.EscapeHTML(w, []byte(content))
+
+				// Write closing tag
+				w.Write([]byte("
")) + return ast.GoToNext, true + } + return ast.GoToNext, false + }, + } + renderer := html.NewRenderer(opts) + + // Parse and render + md := []byte(text) + htmlBytes := markdown.ToHTML(md, p, renderer) + + return string(htmlBytes) +} + +/* +// BACKUP: Old custom markdown renderer - kept for reference +// renderMarkdownToHTML converts simple markdown to HTML +func renderMarkdownToHTML_OLD(text string) string { + lines := strings.Split(text, "\n") + var result strings.Builder + var inCodeBlock bool + var codeBlockContent strings.Builder + var codeLanguage string + + for _, line := range lines { + // Handle code blocks + if strings.HasPrefix(line, "```") { + if !inCodeBlock { + // Starting a code block + inCodeBlock = true + codeLanguage = strings.TrimSpace(line[3:]) + codeBlockContent.Reset() + continue + } else { + // Ending a code block + inCodeBlock = false + codeContent := codeBlockContent.String() + if codeLanguage != "" { + result.WriteString(fmt.Sprintf(`
%s
`, + htmlpkg.EscapeString(codeLanguage), htmlpkg.EscapeString(codeContent))) + } else { + result.WriteString(fmt.Sprintf(`
%s
`, + htmlpkg.EscapeString(codeContent))) + } + codeLanguage = "" + continue + } + } + + // If we're in a code block, accumulate the content + if inCodeBlock { + if codeBlockContent.Len() > 0 { + codeBlockContent.WriteString("\n") + } + codeBlockContent.WriteString(line) + continue + } + + // Normal line processing (when not in code block) + line = strings.TrimSpace(line) + + if line == "" { + result.WriteString("
") + continue + } + + // Handle headings + if strings.HasPrefix(line, "###") { + content := strings.TrimSpace(line[3:]) + result.WriteString(fmt.Sprintf("

%s

", htmlpkg.EscapeString(content))) + continue + } + if strings.HasPrefix(line, "##") { + content := strings.TrimSpace(line[2:]) + result.WriteString(fmt.Sprintf("

%s

", htmlpkg.EscapeString(content))) + continue + } + if strings.HasPrefix(line, "#") { + content := strings.TrimSpace(line[1:]) + result.WriteString(fmt.Sprintf("

%s

", htmlpkg.EscapeString(content))) + continue + } + + // Handle list items + if strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "* ") { + content := strings.TrimSpace(line[2:]) + content = renderInlineMarkdownToHTML(content) + result.WriteString(fmt.Sprintf("", content)) + continue + } + + // Handle blockquotes + if strings.HasPrefix(line, "> ") { + content := strings.TrimSpace(line[2:]) + content = renderInlineMarkdownToHTML(content) + result.WriteString(fmt.Sprintf("
%s
", content)) + continue + } + + // Regular paragraph with inline formatting + line = renderInlineMarkdownToHTML(line) + result.WriteString(fmt.Sprintf("

%s

", line)) + } + + // Handle unclosed code block + if inCodeBlock { + codeContent := codeBlockContent.String() + if codeLanguage != "" { + result.WriteString(fmt.Sprintf(`
%s
`, + htmlpkg.EscapeString(codeLanguage), htmlpkg.EscapeString(codeContent))) + } else { + result.WriteString(fmt.Sprintf(`
%s
`, + htmlpkg.EscapeString(codeContent))) + } + } + + return result.String() +} + +// renderInlineMarkdownToHTML processes inline markdown formatting +func renderInlineMarkdownToHTML_OLD(text string) string { + // Escape HTML first + text = htmlpkg.EscapeString(text) + + // Links with title: [text](url "title") + linkWithTitleRegex := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\s+"([^"]+)"\)`) + text = linkWithTitleRegex.ReplaceAllStringFunc(text, func(match string) string { + matches := linkWithTitleRegex.FindStringSubmatch(match) + if len(matches) == 4 { + linkText := matches[1] + url := matches[2] + title := matches[3] + return fmt.Sprintf(`%s`, url, title, linkText) + } + return match + }) + + // Links without title: [text](url) + linkRegex := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) + text = linkRegex.ReplaceAllStringFunc(text, func(match string) string { + matches := linkRegex.FindStringSubmatch(match) + if len(matches) == 3 { + linkText := matches[1] + url := matches[2] + return fmt.Sprintf(`%s`, url, linkText) + } + return match + }) + + // Bold (**text** or __text__) + boldRegex := regexp.MustCompile(`\*\*(.*?)\*\*`) + text = boldRegex.ReplaceAllString(text, "$1") + + boldRegex2 := regexp.MustCompile(`__(.*?)__`) + text = boldRegex2.ReplaceAllString(text, "$1") + + // Italic (*text* or _text_) + italicRegex := regexp.MustCompile(`\*(.*?)\*`) + text = italicRegex.ReplaceAllString(text, "$1") + + italicRegex2 := regexp.MustCompile(`_(.*?)_`) + text = italicRegex2.ReplaceAllString(text, "$1") + + // Code (`text`) + codeRegex := regexp.MustCompile("`(.*?)`") + text = codeRegex.ReplaceAllString(text, "$1") + + return text +} +*/ diff --git a/backend/main.go b/backend/main.go index 316a937..338dd44 100644 --- a/backend/main.go +++ b/backend/main.go @@ -64,6 +64,7 @@ func main() { mux.HandleFunc("GET /logs/getHistory", middleware.RequireAuth(handlers.GetHistory)) mux.HandleFunc("GET /logs/bookmarkDay", middleware.RequireAuth(handlers.BookmarkDay)) mux.HandleFunc("GET /logs/deleteDay", middleware.RequireAuth(handlers.DeleteDay)) + mux.HandleFunc("GET /logs/exportData", middleware.RequireAuth(handlers.ExportData)) // Create a handler chain with Logger and CORS middleware // Logger middleware will be executed first, then CORS diff --git a/frontend/src/routes/(authed)/+layout.svelte b/frontend/src/routes/(authed)/+layout.svelte index c195c0b..1afcc5c 100644 --- a/frontend/src/routes/(authed)/+layout.svelte +++ b/frontend/src/routes/(authed)/+layout.svelte @@ -581,6 +581,53 @@ } ); } + + let exportPeriod = $state('periodAll'); + let exportStartDate = $state(''); + let exportEndDate = $state(''); + let exportImagesInHTML = $state(true); + let exportSplit = $state('aio'); + let exportTagsInHTML = $state(true); + let isExporting = $state(false); + + function exportData() { + if (isExporting) return; + isExporting = true; + + axios + .get(API_URL + '/logs/exportData', { + params: { + period: exportPeriod, + startDate: exportStartDate, + endDate: exportEndDate, + imagesInHTML: exportImagesInHTML, + split: exportSplit, + tagsInHTML: exportTagsInHTML + }, + responseType: 'blob' // Expect a binary response + }) + .then((response) => { + const blob = new Blob([response.data], { type: 'application/zip' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `DailyTxT_Export_${localStorage.getItem('users')}.zip`; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + }) + .catch((error) => { + console.error(error); + + // show toast + const toast = new bootstrap.Toast(document.getElementById('toastErrorExportData')); + toast.show(); + }) + .finally(() => { + isExporting = false; + }); + }
@@ -1072,7 +1119,152 @@

📁 Daten

-
Export
+
+
Export
+ Exportiere deine Einträge in einer formatierten HTML-Datei. Bilder werden wahlweise + in der HTML eingebunden. Alle Dateien werden außerdem in einer Zip-Datei bereitgestellt. + +
Zeitraum
+
+ + +
+
+ + + {#if exportPeriod === 'periodVariable'} +
+
+ + +
+
+ + +
+
+ {#if exportStartDate !== '' && exportEndDate !== '' && exportStartDate > exportEndDate} + + {/if} + {/if} +
+ +
Anzahl der HTML-Dokumente
+
+ + +
+
+ + +
+
+ + +
+ +
Bilder in HTML anzeigen
+
+ + +
+ +
Tags drucken
+
+ + +
+ +
+ Hinweise: +
    +
  • Die HTML wird keinen Verlauf der einzelnen Tage enthalten.
  • +
  • + Ein Re-Import ist nicht möglich. Diese Funktion dient nicht dem Backup, + sondern rein dem Export, um eine einfach lesbare HTML-Datei zu erhalten. +
  • +
+
+ +
Import
@@ -1457,9 +1649,32 @@
Fehler beim Logout
+ +