2023-09-18 23:57:18 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/md5"
|
|
|
|
"database/sql"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2023-09-23 18:14:57 +00:00
|
|
|
"html"
|
2023-09-18 23:57:18 +00:00
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-09-19 23:29:55 +00:00
|
|
|
"strings"
|
2023-09-18 23:57:18 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
argon2 "github.com/alexedwards/argon2id"
|
|
|
|
"github.com/gabriel-vasile/mimetype"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"golang.org/x/exp/slices"
|
2024-01-11 01:23:36 +00:00
|
|
|
"reichard.io/antholume/database"
|
|
|
|
"reichard.io/antholume/metadata"
|
2023-09-18 23:57:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type activityItem struct {
|
2023-10-03 11:37:14 +00:00
|
|
|
DocumentID string `json:"document"`
|
|
|
|
StartTime int64 `json:"start_time"`
|
|
|
|
Duration int64 `json:"duration"`
|
|
|
|
Page int64 `json:"page"`
|
|
|
|
Pages int64 `json:"pages"`
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type requestActivity struct {
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
Device string `json:"device"`
|
|
|
|
Activity []activityItem `json:"activity"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type requestCheckActivitySync struct {
|
|
|
|
DeviceID string `json:"device_id"`
|
2023-10-03 20:47:38 +00:00
|
|
|
Device string `json:"device"`
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type requestDocument struct {
|
|
|
|
Documents []database.Document `json:"documents"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type requestPosition struct {
|
|
|
|
DocumentID string `json:"document"`
|
|
|
|
Percentage float64 `json:"percentage"`
|
|
|
|
Progress string `json:"progress"`
|
|
|
|
Device string `json:"device"`
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type requestUser struct {
|
|
|
|
Username string `json:"username"`
|
|
|
|
Password string `json:"password"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type requestCheckDocumentSync struct {
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
Device string `json:"device"`
|
|
|
|
Have []string `json:"have"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type responseCheckDocumentSync struct {
|
2023-09-19 23:29:55 +00:00
|
|
|
WantFiles []string `json:"want_files"`
|
|
|
|
WantMetadata []string `json:"want_metadata"`
|
|
|
|
Give []database.Document `json:"give"`
|
|
|
|
Delete []string `json:"deleted"`
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type requestDocumentID struct {
|
|
|
|
DocumentID string `uri:"document" binding:"required"`
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koAuthorizeUser(c *gin.Context) {
|
2023-09-18 23:57:18 +00:00
|
|
|
c.JSON(200, gin.H{
|
|
|
|
"authorized": "OK",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koCreateUser(c *gin.Context) {
|
2023-09-19 23:29:55 +00:00
|
|
|
if !api.Config.RegistrationEnabled {
|
|
|
|
c.AbortWithStatus(http.StatusConflict)
|
2023-09-21 00:35:01 +00:00
|
|
|
return
|
2023-09-19 23:29:55 +00:00
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
var rUser requestUser
|
|
|
|
if err := c.ShouldBindJSON(&rUser); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCreateUser] Invalid JSON Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if rUser.Username == "" || rUser.Password == "" {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCreateUser] Invalid User - Empty Username or Password")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
hashedPassword, err := argon2.CreateHash(rUser.Password, argon2.DefaultParams)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCreateUser] Argon2 Hash Failure:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
|
|
|
ID: rUser.Username,
|
2023-09-27 22:58:47 +00:00
|
|
|
Pass: &hashedPassword,
|
2023-09-18 23:57:18 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCreateUser] CreateUser DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-21 00:35:01 +00:00
|
|
|
// User Exists
|
2023-09-18 23:57:18 +00:00
|
|
|
if rows == 0 {
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "User Already Exists"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
|
|
"username": rUser.Username,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koSetProgress(c *gin.Context) {
|
2024-01-10 02:08:40 +00:00
|
|
|
var auth authData
|
|
|
|
if data, _ := c.Get("Authorization"); data != nil {
|
|
|
|
auth = data.(authData)
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
var rPosition requestPosition
|
|
|
|
if err := c.ShouldBindJSON(&rPosition); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koSetProgress] Invalid JSON Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Progress Data"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-21 17:50:25 +00:00
|
|
|
start := time.Now()
|
2023-09-18 23:57:18 +00:00
|
|
|
// Upsert Device
|
2023-09-21 00:35:01 +00:00
|
|
|
if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
ID: rPosition.DeviceID,
|
2024-01-10 02:08:40 +00:00
|
|
|
UserID: auth.UserName,
|
2023-09-18 23:57:18 +00:00
|
|
|
DeviceName: rPosition.Device,
|
2023-10-10 23:06:12 +00:00
|
|
|
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
2023-09-21 00:35:01 +00:00
|
|
|
}); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koSetProgress] UpsertDevice DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
2024-01-21 17:50:25 +00:00
|
|
|
log.Debug("[koSetProgress] UpsertDevice Performance: ", time.Since(start))
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2024-01-21 17:50:25 +00:00
|
|
|
start = time.Now()
|
2023-09-18 23:57:18 +00:00
|
|
|
// Upsert Document
|
2023-09-21 00:35:01 +00:00
|
|
|
if _, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
ID: rPosition.DocumentID,
|
2023-09-21 00:35:01 +00:00
|
|
|
}); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koSetProgress] UpsertDocument DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
2024-01-21 17:50:25 +00:00
|
|
|
log.Debug("[koSetProgress] UpsertDocument Performance: ", time.Since(start))
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2024-01-21 17:50:25 +00:00
|
|
|
start = time.Now()
|
2023-09-18 23:57:18 +00:00
|
|
|
// Create or Replace Progress
|
|
|
|
progress, err := api.DB.Queries.UpdateProgress(api.DB.Ctx, database.UpdateProgressParams{
|
|
|
|
Percentage: rPosition.Percentage,
|
|
|
|
DocumentID: rPosition.DocumentID,
|
|
|
|
DeviceID: rPosition.DeviceID,
|
2024-01-10 02:08:40 +00:00
|
|
|
UserID: auth.UserName,
|
2023-09-18 23:57:18 +00:00
|
|
|
Progress: rPosition.Progress,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koSetProgress] UpdateProgress DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
2024-01-21 17:50:25 +00:00
|
|
|
log.Debug("[koSetProgress] UpdateProgress Performance: ", time.Since(start))
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
"document": progress.DocumentID,
|
|
|
|
"timestamp": progress.CreatedAt,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koGetProgress(c *gin.Context) {
|
2024-01-10 02:08:40 +00:00
|
|
|
var auth authData
|
|
|
|
if data, _ := c.Get("Authorization"); data != nil {
|
|
|
|
auth = data.(authData)
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
var rDocID requestDocumentID
|
|
|
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koGetProgress] Invalid URI Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-21 17:50:25 +00:00
|
|
|
start := time.Now()
|
2024-01-01 04:12:46 +00:00
|
|
|
progress, err := api.DB.Queries.GetDocumentProgress(api.DB.Ctx, database.GetDocumentProgressParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
DocumentID: rDocID.DocumentID,
|
2024-01-10 02:08:40 +00:00
|
|
|
UserID: auth.UserName,
|
2023-09-18 23:57:18 +00:00
|
|
|
})
|
2024-01-21 17:50:25 +00:00
|
|
|
log.Debug("[koGetProgress] GetDocumentProgress Performance: ", time.Since(start))
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2023-10-10 23:06:12 +00:00
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
// Not Found
|
|
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
|
|
return
|
|
|
|
} else if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koGetProgress] GetDocumentProgress DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
"document": progress.DocumentID,
|
|
|
|
"percentage": progress.Percentage,
|
|
|
|
"progress": progress.Progress,
|
|
|
|
"device": progress.DeviceName,
|
|
|
|
"device_id": progress.DeviceID,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koAddActivities(c *gin.Context) {
|
2024-01-10 02:08:40 +00:00
|
|
|
var auth authData
|
|
|
|
if data, _ := c.Get("Authorization"); data != nil {
|
|
|
|
auth = data.(authData)
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
var rActivity requestActivity
|
|
|
|
if err := c.ShouldBindJSON(&rActivity); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddActivities] Invalid JSON Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do Transaction
|
|
|
|
tx, err := api.DB.DB.Begin()
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddActivities] Transaction Begin DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Derive Unique Documents
|
|
|
|
allDocumentsMap := make(map[string]bool)
|
|
|
|
for _, item := range rActivity.Activity {
|
|
|
|
allDocumentsMap[item.DocumentID] = true
|
|
|
|
}
|
|
|
|
allDocuments := getKeys(allDocumentsMap)
|
|
|
|
|
|
|
|
// Defer & Start Transaction
|
|
|
|
defer tx.Rollback()
|
|
|
|
qtx := api.DB.Queries.WithTx(tx)
|
|
|
|
|
|
|
|
// Upsert Documents
|
|
|
|
for _, doc := range allDocuments {
|
2023-09-21 00:35:01 +00:00
|
|
|
if _, err := qtx.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
ID: doc,
|
2023-09-21 00:35:01 +00:00
|
|
|
}); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddActivities] UpsertDocument DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upsert Device
|
2023-09-21 00:35:01 +00:00
|
|
|
if _, err = qtx.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
ID: rActivity.DeviceID,
|
2024-01-10 02:08:40 +00:00
|
|
|
UserID: auth.UserName,
|
2023-09-18 23:57:18 +00:00
|
|
|
DeviceName: rActivity.Device,
|
2023-10-10 23:06:12 +00:00
|
|
|
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
2023-09-21 00:35:01 +00:00
|
|
|
}); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddActivities] UpsertDevice DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add All Activity
|
|
|
|
for _, item := range rActivity.Activity {
|
2023-09-21 00:35:01 +00:00
|
|
|
if _, err := qtx.AddActivity(api.DB.Ctx, database.AddActivityParams{
|
2024-01-10 02:08:40 +00:00
|
|
|
UserID: auth.UserName,
|
2023-11-03 23:38:31 +00:00
|
|
|
DocumentID: item.DocumentID,
|
|
|
|
DeviceID: rActivity.DeviceID,
|
|
|
|
StartTime: time.Unix(int64(item.StartTime), 0).UTC().Format(time.RFC3339),
|
|
|
|
Duration: int64(item.Duration),
|
|
|
|
StartPercentage: float64(item.Page) / float64(item.Pages),
|
|
|
|
EndPercentage: float64(item.Page+1) / float64(item.Pages),
|
2023-09-21 00:35:01 +00:00
|
|
|
}); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddActivities] AddActivity DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Commit Transaction
|
2023-09-21 00:35:01 +00:00
|
|
|
if err := tx.Commit(); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddActivities] Transaction Commit DB Error:", err)
|
2023-09-21 00:35:01 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
"added": len(rActivity.Activity),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koCheckActivitySync(c *gin.Context) {
|
2024-01-10 02:08:40 +00:00
|
|
|
var auth authData
|
|
|
|
if data, _ := c.Get("Authorization"); data != nil {
|
|
|
|
auth = data.(authData)
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
var rCheckActivity requestCheckActivitySync
|
|
|
|
if err := c.ShouldBindJSON(&rCheckActivity); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckActivitySync] Invalid JSON Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-03 20:47:38 +00:00
|
|
|
// Upsert Device
|
|
|
|
if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
|
|
|
ID: rCheckActivity.DeviceID,
|
2024-01-10 02:08:40 +00:00
|
|
|
UserID: auth.UserName,
|
2023-10-03 20:47:38 +00:00
|
|
|
DeviceName: rCheckActivity.Device,
|
2023-10-06 01:04:57 +00:00
|
|
|
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
2023-10-03 20:47:38 +00:00
|
|
|
}); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckActivitySync] UpsertDevice DB Error", err)
|
2023-10-03 20:47:38 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
// Get Last Device Activity
|
|
|
|
lastActivity, err := api.DB.Queries.GetLastActivity(api.DB.Ctx, database.GetLastActivityParams{
|
2024-01-10 02:08:40 +00:00
|
|
|
UserID: auth.UserName,
|
2023-09-18 23:57:18 +00:00
|
|
|
DeviceID: rCheckActivity.DeviceID,
|
|
|
|
})
|
|
|
|
if err == sql.ErrNoRows {
|
2023-10-06 01:04:57 +00:00
|
|
|
lastActivity = time.UnixMilli(0).Format(time.RFC3339)
|
2023-09-18 23:57:18 +00:00
|
|
|
} else if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckActivitySync] GetLastActivity DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-06 01:04:57 +00:00
|
|
|
// Parse Time
|
|
|
|
parsedTime, err := time.Parse(time.RFC3339, lastActivity)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckActivitySync] Time Parse Error:", err)
|
2023-10-06 01:04:57 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
c.JSON(http.StatusOK, gin.H{
|
2023-10-06 01:04:57 +00:00
|
|
|
"last_sync": parsedTime.Unix(),
|
2023-09-18 23:57:18 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koAddDocuments(c *gin.Context) {
|
2023-09-18 23:57:18 +00:00
|
|
|
var rNewDocs requestDocument
|
|
|
|
if err := c.ShouldBindJSON(&rNewDocs); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddDocuments] Invalid JSON Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document(s)"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do Transaction
|
|
|
|
tx, err := api.DB.DB.Begin()
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddDocuments] Transaction Begin DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Defer & Start Transaction
|
|
|
|
defer tx.Rollback()
|
|
|
|
qtx := api.DB.Queries.WithTx(tx)
|
|
|
|
|
|
|
|
// Upsert Documents
|
|
|
|
for _, doc := range rNewDocs.Documents {
|
2023-11-03 23:38:31 +00:00
|
|
|
_, err := qtx.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
ID: doc.ID,
|
2023-09-23 02:12:36 +00:00
|
|
|
Title: api.sanitizeInput(doc.Title),
|
|
|
|
Author: api.sanitizeInput(doc.Author),
|
|
|
|
Series: api.sanitizeInput(doc.Series),
|
2023-09-18 23:57:18 +00:00
|
|
|
SeriesIndex: doc.SeriesIndex,
|
2023-09-23 02:12:36 +00:00
|
|
|
Lang: api.sanitizeInput(doc.Lang),
|
|
|
|
Description: api.sanitizeInput(doc.Description),
|
2023-09-18 23:57:18 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddDocuments] UpsertDocument DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Commit Transaction
|
2023-09-21 00:35:01 +00:00
|
|
|
if err := tx.Commit(); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koAddDocuments] Transaction Commit DB Error:", err)
|
2023-09-21 00:35:01 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
|
|
|
return
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
"changed": len(rNewDocs.Documents),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
2024-01-10 02:08:40 +00:00
|
|
|
var auth authData
|
|
|
|
if data, _ := c.Get("Authorization"); data != nil {
|
|
|
|
auth = data.(authData)
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
var rCheckDocs requestCheckDocumentSync
|
|
|
|
if err := c.ShouldBindJSON(&rCheckDocs); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckDocumentsSync] Invalid JSON Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upsert Device
|
2023-11-03 23:38:31 +00:00
|
|
|
_, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
ID: rCheckDocs.DeviceID,
|
2024-01-10 02:08:40 +00:00
|
|
|
UserID: auth.UserName,
|
2023-09-18 23:57:18 +00:00
|
|
|
DeviceName: rCheckDocs.Device,
|
2023-10-06 01:04:57 +00:00
|
|
|
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
2023-09-18 23:57:18 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckDocumentsSync] UpsertDevice DB Error", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
missingDocs := []database.Document{}
|
|
|
|
deletedDocIDs := []string{}
|
|
|
|
|
2023-11-03 23:38:31 +00:00
|
|
|
// Get Missing Documents
|
|
|
|
missingDocs, err = api.DB.Queries.GetMissingDocuments(api.DB.Ctx, rCheckDocs.Have)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckDocumentsSync] GetMissingDocuments DB Error", err)
|
2023-11-03 23:38:31 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2023-11-03 23:38:31 +00:00
|
|
|
// Get Deleted Documents
|
|
|
|
deletedDocIDs, err = api.DB.Queries.GetDeletedDocuments(api.DB.Ctx, rCheckDocs.Have)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckDocumentsSync] GetDeletedDocuments DB Error", err)
|
2023-11-03 23:38:31 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get Wanted Documents
|
|
|
|
jsonHaves, err := json.Marshal(rCheckDocs.Have)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckDocumentsSync] JSON Marshal Error", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-19 23:29:55 +00:00
|
|
|
wantedDocs, err := api.DB.Queries.GetWantedDocuments(api.DB.Ctx, string(jsonHaves))
|
2023-09-18 23:57:18 +00:00
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koCheckDocumentsSync] GetWantedDocuments DB Error", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-19 23:29:55 +00:00
|
|
|
// Split Metadata & File Wants
|
|
|
|
var wantedMetadataDocIDs []string
|
|
|
|
var wantedFilesDocIDs []string
|
|
|
|
for _, v := range wantedDocs {
|
|
|
|
if v.WantMetadata {
|
|
|
|
wantedMetadataDocIDs = append(wantedMetadataDocIDs, v.ID)
|
|
|
|
}
|
|
|
|
if v.WantFile {
|
|
|
|
wantedFilesDocIDs = append(wantedFilesDocIDs, v.ID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
rCheckDocSync := responseCheckDocumentSync{
|
2023-09-19 23:29:55 +00:00
|
|
|
Delete: []string{},
|
|
|
|
WantFiles: []string{},
|
|
|
|
WantMetadata: []string{},
|
|
|
|
Give: []database.Document{},
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure Empty Array
|
2023-09-19 23:29:55 +00:00
|
|
|
if wantedMetadataDocIDs != nil {
|
|
|
|
rCheckDocSync.WantMetadata = wantedMetadataDocIDs
|
|
|
|
}
|
|
|
|
if wantedFilesDocIDs != nil {
|
|
|
|
rCheckDocSync.WantFiles = wantedFilesDocIDs
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
if missingDocs != nil {
|
|
|
|
rCheckDocSync.Give = missingDocs
|
|
|
|
}
|
|
|
|
if deletedDocIDs != nil {
|
|
|
|
rCheckDocSync.Delete = deletedDocIDs
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, rCheckDocSync)
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koUploadExistingDocument(c *gin.Context) {
|
2023-09-18 23:57:18 +00:00
|
|
|
var rDoc requestDocumentID
|
|
|
|
if err := c.ShouldBindUri(&rDoc); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koUploadExistingDocument] Invalid URI Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fileData, err := c.FormFile("file")
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koUploadExistingDocument] File Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate Type & Derive Extension on MIME
|
|
|
|
uploadedFile, err := fileData.Open()
|
|
|
|
fileMime, err := mimetype.DetectReader(uploadedFile)
|
|
|
|
fileExtension := fileMime.Extension()
|
|
|
|
|
2023-09-23 18:14:57 +00:00
|
|
|
if !slices.Contains([]string{".epub", ".html"}, fileExtension) {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koUploadExistingDocument] Invalid FileType:", fileExtension)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate Document Exists in DB
|
|
|
|
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koUploadExistingDocument] GetDocument DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Derive Filename
|
|
|
|
var fileName string
|
|
|
|
if document.Author != nil {
|
|
|
|
fileName = fileName + *document.Author
|
|
|
|
} else {
|
|
|
|
fileName = fileName + "Unknown"
|
|
|
|
}
|
|
|
|
|
|
|
|
if document.Title != nil {
|
|
|
|
fileName = fileName + " - " + *document.Title
|
|
|
|
} else {
|
|
|
|
fileName = fileName + " - Unknown"
|
|
|
|
}
|
|
|
|
|
2023-09-19 23:29:55 +00:00
|
|
|
// Remove Slashes
|
|
|
|
fileName = strings.ReplaceAll(fileName, "/", "")
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
// Derive & Sanitize File Name
|
|
|
|
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, document.ID, fileExtension))
|
|
|
|
|
|
|
|
// Generate Storage Path
|
|
|
|
safePath := filepath.Join(api.Config.DataPath, "documents", fileName)
|
|
|
|
|
|
|
|
// Save & Prevent Overwrites
|
|
|
|
_, err = os.Stat(safePath)
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
err = c.SaveUploadedFile(fileData, safePath)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koUploadExistingDocument] Save Failure:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get MD5 Hash
|
|
|
|
fileHash, err := getFileMD5(safePath)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koUploadExistingDocument] Hash Failure:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-11-03 23:38:31 +00:00
|
|
|
// Get Word Count
|
|
|
|
wordCount, err := metadata.GetWordCount(safePath)
|
|
|
|
if err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koUploadExistingDocument] Word Count Failure:", err)
|
2023-11-03 23:38:31 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
// Upsert Document
|
2023-09-21 00:35:01 +00:00
|
|
|
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
ID: document.ID,
|
|
|
|
Md5: fileHash,
|
|
|
|
Filepath: &fileName,
|
2023-11-03 23:38:31 +00:00
|
|
|
Words: &wordCount,
|
2023-09-21 00:35:01 +00:00
|
|
|
}); err != nil {
|
2024-01-01 04:12:46 +00:00
|
|
|
log.Error("[koUploadExistingDocument] UpsertDocument DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
"status": "ok",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-01 04:12:46 +00:00
|
|
|
func (api *API) koDemoModeJSONError(c *gin.Context) {
|
|
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Not Allowed in Demo Mode"})
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
2023-09-23 02:12:36 +00:00
|
|
|
func (api *API) sanitizeInput(val any) *string {
|
|
|
|
switch v := val.(type) {
|
|
|
|
case *string:
|
|
|
|
if v != nil {
|
2023-09-23 18:14:57 +00:00
|
|
|
newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(*v)))
|
2023-09-23 02:12:36 +00:00
|
|
|
return &newString
|
|
|
|
}
|
|
|
|
case string:
|
|
|
|
if v != "" {
|
2023-09-23 18:14:57 +00:00
|
|
|
newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(v)))
|
2023-09-23 02:12:36 +00:00
|
|
|
return &newString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
func getKeys[M ~map[K]V, K comparable, V any](m M) []K {
|
|
|
|
r := make([]K, 0, len(m))
|
|
|
|
for k := range m {
|
|
|
|
r = append(r, k)
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
func getFileMD5(filePath string) (*string, error) {
|
|
|
|
file, err := os.Open(filePath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
hash := md5.New()
|
|
|
|
_, err = io.Copy(hash, file)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
fileHash := fmt.Sprintf("%x", hash.Sum(nil))
|
|
|
|
|
|
|
|
return &fileHash, nil
|
|
|
|
}
|