[add] progress streaming
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
13
api/api.go
13
api/api.go
@@ -26,15 +26,16 @@ type API struct {
|
||||
DB *database.DBManager
|
||||
HTMLPolicy *bluemonday.Policy
|
||||
Assets *embed.FS
|
||||
Templates map[string]*template.Template
|
||||
}
|
||||
|
||||
func NewApi(db *database.DBManager, c *config.Config, assets embed.FS) *API {
|
||||
func NewApi(db *database.DBManager, c *config.Config, assets *embed.FS) *API {
|
||||
api := &API{
|
||||
HTMLPolicy: bluemonday.StrictPolicy(),
|
||||
Router: gin.Default(),
|
||||
Config: c,
|
||||
DB: db,
|
||||
Assets: &assets,
|
||||
Assets: assets,
|
||||
}
|
||||
|
||||
// Assets & Web App Templates
|
||||
@@ -168,6 +169,7 @@ func (api *API) registerOPDSRoutes(apiGroup *gin.RouterGroup) {
|
||||
|
||||
func (api *API) generateTemplates() *multitemplate.Renderer {
|
||||
// Define Templates & Helper Functions
|
||||
templates := make(map[string]*template.Template)
|
||||
render := multitemplate.NewRenderer()
|
||||
helperFuncs := template.FuncMap{
|
||||
"GetSVGGraphData": getSVGGraphData,
|
||||
@@ -189,6 +191,7 @@ func (api *API) generateTemplates() *multitemplate.Renderer {
|
||||
|
||||
b, _ := api.Assets.ReadFile(path)
|
||||
baseTemplate = template.Must(baseTemplate.New("svg/" + name).Parse(string(b)))
|
||||
templates["svg/"+name] = baseTemplate
|
||||
}
|
||||
|
||||
// Load Components
|
||||
@@ -198,8 +201,11 @@ func (api *API) generateTemplates() *multitemplate.Renderer {
|
||||
path := fmt.Sprintf("templates/components/%s", basename)
|
||||
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
|
||||
// Clone Base Template
|
||||
b, _ := api.Assets.ReadFile(path)
|
||||
baseTemplate = template.Must(baseTemplate.New("component/" + name).Parse(string(b)))
|
||||
render.Add("component/"+name, baseTemplate)
|
||||
templates["component/"+name] = baseTemplate
|
||||
}
|
||||
|
||||
// Load Pages
|
||||
@@ -213,8 +219,11 @@ func (api *API) generateTemplates() *multitemplate.Renderer {
|
||||
b, _ := api.Assets.ReadFile(path)
|
||||
pageTemplate, _ := template.Must(baseTemplate.Clone()).New("page/" + name).Parse(string(b))
|
||||
render.Add("page/"+name, pageTemplate)
|
||||
templates["page/"+name] = pageTemplate
|
||||
}
|
||||
|
||||
api.Templates = templates
|
||||
|
||||
return &render
|
||||
}
|
||||
|
||||
|
||||
@@ -773,22 +773,63 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Render Initial Template
|
||||
var userID string
|
||||
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
||||
userID = rUser.(string)
|
||||
}
|
||||
templateVars := gin.H{
|
||||
"RouteName": "search",
|
||||
"SearchEnabled": api.Config.SearchEnabled,
|
||||
"User": userID,
|
||||
}
|
||||
c.HTML(http.StatusOK, "page/search", templateVars)
|
||||
|
||||
// Create Streamer
|
||||
stream := api.newStreamer(c)
|
||||
defer stream.close()
|
||||
|
||||
// Stream Helper Function
|
||||
sendDownloadMessage := func(msg string, args ...map[string]any) {
|
||||
// Merge Defaults & Overrides
|
||||
var templateVars = gin.H{
|
||||
"Message": msg,
|
||||
"ButtonText": "Close",
|
||||
"ButtonHref": "./search",
|
||||
}
|
||||
if len(args) > 0 {
|
||||
for key := range args[0] {
|
||||
templateVars[key] = args[0][key]
|
||||
}
|
||||
}
|
||||
|
||||
stream.send("component/download-progress", templateVars)
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Downloading document...", gin.H{"Progress": 10})
|
||||
|
||||
// Save Book
|
||||
tempFilePath, err := search.SaveBook(rDocAdd.ID, rDocAdd.Source)
|
||||
if err != nil {
|
||||
log.Warn("[saveNewDocument] Temp File Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
sendDownloadMessage("Unable to download file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Calculating partial MD5...", gin.H{"Progress": 60})
|
||||
|
||||
// Calculate Partial MD5 ID
|
||||
partialMD5, err := utils.CalculatePartialMD5(tempFilePath)
|
||||
if err != nil {
|
||||
log.Warn("[saveNewDocument] Partial MD5 Error: ", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate partial MD5.")
|
||||
return
|
||||
sendDownloadMessage("Unable to calculate partial MD5", gin.H{"Error": true})
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Saving file...", gin.H{"Progress": 60})
|
||||
|
||||
// Derive Extension on MIME
|
||||
fileMime, err := mimetype.DetectFile(tempFilePath)
|
||||
fileExtension := fileMime.Extension()
|
||||
@@ -817,7 +858,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
sourceFile, err := os.Open(tempFilePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Source File Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
sendDownloadMessage("Unable to open file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
defer os.Remove(tempFilePath)
|
||||
@@ -828,7 +869,7 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
destFile, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Dest File Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
sendDownloadMessage("Unable to create file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
defer destFile.Close()
|
||||
@@ -836,26 +877,35 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
// Copy File
|
||||
if _, err = io.Copy(destFile, sourceFile); err != nil {
|
||||
log.Error("[saveNewDocument] Copy Temp File Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||
sendDownloadMessage("Unable to save file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Calculating MD5...", gin.H{"Progress": 70})
|
||||
|
||||
// Get MD5 Hash
|
||||
fileHash, err := getFileMD5(safePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Hash Failure:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate MD5.")
|
||||
sendDownloadMessage("Unable to calculate MD5", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Calculating word count...", gin.H{"Progress": 80})
|
||||
|
||||
// Get Word Count
|
||||
wordCount, err := metadata.GetWordCount(safePath)
|
||||
if err != nil {
|
||||
log.Error("[saveNewDocument] Word Count Failure:", err)
|
||||
errorPage(c, http.StatusInternalServerError, "Unable to calculate word count.")
|
||||
sendDownloadMessage("Unable to calculate word count", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
|
||||
// Send Message
|
||||
sendDownloadMessage("Saving to database...", gin.H{"Progress": 90})
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: partialMD5,
|
||||
@@ -866,11 +916,16 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
||||
Words: &wordCount,
|
||||
}); err != nil {
|
||||
log.Error("[saveNewDocument] UpsertDocument DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
sendDownloadMessage("Unable to save to database", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", partialMD5))
|
||||
// Send Message
|
||||
sendDownloadMessage("Download Success", gin.H{
|
||||
"Progress": 100,
|
||||
"ButtonText": "Go to Book",
|
||||
"ButtonHref": fmt.Sprintf("./documents/%s", partialMD5),
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) editSettings(c *gin.Context) {
|
||||
|
||||
79
api/streamer.go
Normal file
79
api/streamer.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type streamer struct {
|
||||
templates map[string]*template.Template
|
||||
writer gin.ResponseWriter
|
||||
mutex sync.Mutex
|
||||
completeCh chan struct{}
|
||||
}
|
||||
|
||||
func (api *API) newStreamer(c *gin.Context) *streamer {
|
||||
stream := &streamer{
|
||||
templates: api.Templates,
|
||||
writer: c.Writer,
|
||||
completeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Set Headers
|
||||
header := stream.writer.Header()
|
||||
header.Set("Transfer-Encoding", "chunked")
|
||||
header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
stream.writer.WriteHeader(http.StatusOK)
|
||||
|
||||
// Send Open Element Tags
|
||||
stream.write(`
|
||||
<div class="absolute top-0 left-0 w-full h-full z-50">
|
||||
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
|
||||
<div id="stream-main" class="relative max-h-[95%] -translate-x-2/4 top-1/2 left-1/2 w-5/6">`)
|
||||
|
||||
// Keep Alive
|
||||
go func() {
|
||||
closeCh := stream.writer.CloseNotify()
|
||||
for {
|
||||
select {
|
||||
case <-stream.completeCh:
|
||||
return
|
||||
case <-closeCh:
|
||||
return
|
||||
default:
|
||||
stream.write("<!-- ping -->")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
func (stream *streamer) write(str string) {
|
||||
stream.mutex.Lock()
|
||||
stream.writer.WriteString(str)
|
||||
stream.writer.(http.Flusher).Flush()
|
||||
stream.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (stream *streamer) send(templateName string, templateVars gin.H) {
|
||||
t := stream.templates[templateName]
|
||||
buf := &bytes.Buffer{}
|
||||
_ = t.ExecuteTemplate(buf, templateName, templateVars)
|
||||
stream.write(buf.String())
|
||||
}
|
||||
|
||||
func (stream *streamer) close() {
|
||||
// Send Close Element Tags
|
||||
stream.write(`</div></div>`)
|
||||
|
||||
// Close
|
||||
close(stream.completeCh)
|
||||
}
|
||||
Reference in New Issue
Block a user