feat: add ui and handler for export

pull/23/head
Gabe Farrell 6 months ago
parent 3157db8b6d
commit f14c25c52f

@ -196,6 +196,8 @@ function deleteListen(listen: Listen): Promise<Response> {
method: "DELETE" method: "DELETE"
}) })
} }
function getExport() {
}
export { export {
getLastListens, getLastListens,
@ -224,6 +226,7 @@ export {
updateApiKeyLabel, updateApiKeyLabel,
deleteListen, deleteListen,
getAlbum, getAlbum,
getExport,
} }
type Track = { type Track = {
id: number id: number

@ -0,0 +1,45 @@
import { useState } from "react";
import { AsyncButton } from "../AsyncButton";
import { getExport } from "api/api";
export default function ExportModal() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleExport = () => {
setLoading(true)
fetch(`/apis/web/v1/export`, {
method: "GET"
})
.then(res => {
if (res.ok) {
res.blob()
.then(blob => {
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = "koito_export.json"
document.body.appendChild(a)
a.click()
a.remove()
window.URL.revokeObjectURL(url)
setLoading(false)
})
} else {
res.json().then(r => setError(r.error))
setLoading(false)
}
}).catch(err => {
setError(err)
setLoading(false)
})
}
return (
<div>
<h2>Export</h2>
<AsyncButton loading={loading} onClick={handleExport}>Export Data</AsyncButton>
{error && <p className="error">{error}</p>}
</div>
)
}

@ -5,6 +5,8 @@ import { ThemeSwitcher } from "../themeSwitcher/ThemeSwitcher";
import ThemeHelper from "../../routes/ThemeHelper"; import ThemeHelper from "../../routes/ThemeHelper";
import { useAppContext } from "~/providers/AppProvider"; import { useAppContext } from "~/providers/AppProvider";
import ApiKeysModal from "./ApiKeysModal"; import ApiKeysModal from "./ApiKeysModal";
import { AsyncButton } from "../AsyncButton";
import ExportModal from "./ExportModal";
interface Props { interface Props {
open: boolean open: boolean
@ -29,9 +31,12 @@ export default function SettingsModal({ open, setOpen } : Props) {
<TabsTrigger className={triggerClasses} value="Appearance">Appearance</TabsTrigger> <TabsTrigger className={triggerClasses} value="Appearance">Appearance</TabsTrigger>
<TabsTrigger className={triggerClasses} value="Account">Account</TabsTrigger> <TabsTrigger className={triggerClasses} value="Account">Account</TabsTrigger>
{user && ( {user && (
<>
<TabsTrigger className={triggerClasses} value="API Keys"> <TabsTrigger className={triggerClasses} value="API Keys">
API Keys API Keys
</TabsTrigger> </TabsTrigger>
<TabsTrigger className={triggerClasses} value="Export">Export</TabsTrigger>
</>
)} )}
</TabsList> </TabsList>
@ -44,6 +49,9 @@ export default function SettingsModal({ open, setOpen } : Props) {
<TabsContent value="API Keys" className={contentClasses}> <TabsContent value="API Keys" className={contentClasses}>
<ApiKeysModal /> <ApiKeysModal />
</TabsContent> </TabsContent>
<TabsContent value="Export" className={contentClasses}>
<ExportModal />
</TabsContent>
</Tabs> </Tabs>
</Modal> </Modal>
) )

@ -0,0 +1,33 @@
package handlers
import (
"net/http"
"github.com/gabehf/koito/engine/middleware"
"github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/export"
"github.com/gabehf/koito/internal/logger"
"github.com/gabehf/koito/internal/utils"
)
func ExportHandler(store db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Disposition", `attachment; filename="koito_export.json"`)
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("ExportHandler: Recieved request for export file")
u := middleware.GetUserFromContext(ctx)
if u == nil {
l.Debug().Msg("ExportHandler: Unauthorized access")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
err := export.ExportData(ctx, u, store, w)
if err != nil {
l.Err(err).Msg("ExportHandler: Failed to create export file")
utils.WriteError(w, "failed to create export file", http.StatusInternalServerError)
return
}
}
}

@ -70,6 +70,7 @@ func bindRoutes(
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(middleware.ValidateSession(db)) r.Use(middleware.ValidateSession(db))
r.Get("/export", handlers.ExportHandler(db))
r.Post("/replace-image", handlers.ReplaceImageHandler(db)) r.Post("/replace-image", handlers.ReplaceImageHandler(db))
r.Patch("/album", handlers.UpdateAlbumHandler(db)) r.Patch("/album", handlers.UpdateAlbumHandler(db))
r.Post("/merge/tracks", handlers.MergeTracksHandler(db)) r.Post("/merge/tracks", handlers.MergeTracksHandler(db))

@ -4,11 +4,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "io"
"path"
"time" "time"
"github.com/gabehf/koito/internal/cfg"
"github.com/gabehf/koito/internal/db" "github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/logger" "github.com/gabehf/koito/internal/logger"
"github.com/gabehf/koito/internal/models" "github.com/gabehf/koito/internal/models"
@ -45,7 +43,7 @@ type KoitoArtist struct {
Aliases []models.Alias `json:"aliases"` Aliases []models.Alias `json:"aliases"`
} }
func ExportData(ctx context.Context, username string, store db.DB) error { func ExportData(ctx context.Context, user *models.User, store db.DB, out io.Writer) error {
lastTime := time.Unix(0, 0) lastTime := time.Unix(0, 0)
lastTrackId := int32(0) lastTrackId := int32(0)
pageSize := int32(1000) pageSize := int32(1000)
@ -54,15 +52,15 @@ func ExportData(ctx context.Context, username string, store db.DB) error {
l.Info().Msg("ExportData: Generating Koito export file...") l.Info().Msg("ExportData: Generating Koito export file...")
exportedAt := time.Now() exportedAt := time.Now()
exportFile := path.Join(cfg.ConfigDir(), fmt.Sprintf("koito_export_%d.json", exportedAt.Unix())) // exportFile := path.Join(cfg.ConfigDir(), fmt.Sprintf("koito_export_%d.json", exportedAt.Unix()))
f, err := os.Create(exportFile) // f, err := os.Create(exportFile)
if err != nil { // if err != nil {
return fmt.Errorf("ExportData: %w", err) // return fmt.Errorf("ExportData: %w", err)
} // }
defer f.Close() // defer f.Close()
// Write the opening of the JSON manually // Write the opening of the JSON manually
_, err = fmt.Fprintf(f, "{\n \"version\": \"1\",\n \"exported_at\": \"%s\",\n \"user\": \"%s\",\n \"listens\": [\n", exportedAt.UTC().Format(time.RFC3339), username) _, err := fmt.Fprintf(out, "{\n \"version\": \"1\",\n \"exported_at\": \"%s\",\n \"user\": \"%s\",\n \"listens\": [\n", exportedAt.UTC().Format(time.RFC3339), user.Username)
if err != nil { if err != nil {
return fmt.Errorf("ExportData: %w", err) return fmt.Errorf("ExportData: %w", err)
} }
@ -70,7 +68,7 @@ func ExportData(ctx context.Context, username string, store db.DB) error {
first := true first := true
for { for {
rows, err := store.GetExportPage(ctx, db.GetExportPageOpts{ rows, err := store.GetExportPage(ctx, db.GetExportPageOpts{
UserID: 1, UserID: user.ID,
ListenedAt: lastTime, ListenedAt: lastTime,
TrackID: lastTrackId, TrackID: lastTrackId,
Limit: pageSize, Limit: pageSize,
@ -85,7 +83,7 @@ func ExportData(ctx context.Context, username string, store db.DB) error {
for _, r := range rows { for _, r := range rows {
// Adds a comma after each listen item // Adds a comma after each listen item
if !first { if !first {
_, _ = f.Write([]byte(",\n")) _, _ = out.Write([]byte(",\n"))
} }
first = false first = false
@ -94,12 +92,12 @@ func ExportData(ctx context.Context, username string, store db.DB) error {
raw, err := json.MarshalIndent(exported, " ", " ") raw, err := json.MarshalIndent(exported, " ", " ")
// needed to make the listen item start at the right indent level // needed to make the listen item start at the right indent level
f.Write([]byte(" ")) out.Write([]byte(" "))
if err != nil { if err != nil {
return fmt.Errorf("ExportData: marshal: %w", err) return fmt.Errorf("ExportData: marshal: %w", err)
} }
_, _ = f.Write(raw) _, _ = out.Write(raw)
if r.TrackID > lastTrackId { if r.TrackID > lastTrackId {
lastTrackId = r.TrackID lastTrackId = r.TrackID
@ -111,12 +109,12 @@ func ExportData(ctx context.Context, username string, store db.DB) error {
} }
// Write closing of the JSON array and object // Write closing of the JSON array and object
_, err = f.Write([]byte("\n ]\n}\n")) _, err = out.Write([]byte("\n ]\n}\n"))
if err != nil { if err != nil {
return fmt.Errorf("ExportData: f.Write: %w", err) return fmt.Errorf("ExportData: f.Write: %w", err)
} }
l.Info().Msgf("Export successful! File can be found at %s", exportFile) l.Info().Msgf("Export successfully created")
return nil return nil
} }

Loading…
Cancel
Save