478 lines
13 KiB
Go
478 lines
13 KiB
Go
// sysop_libraries.go implements file library management for the sysop menu.
|
|
//
|
|
// The original TAG-BBS managed libraries through GENERATE config files.
|
|
// Each library had a name, disk path, upload/download security ranges,
|
|
// and a maximum file count. Our version provides full CRUD from the
|
|
// sysop menu.
|
|
//
|
|
// Libraries are slightly more complex than boards or bulletins because
|
|
// they have both an upload and download security range, plus an on-disk
|
|
// file path where actual uploaded files are stored.
|
|
package menu
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/urit/urit/internal/models"
|
|
"github.com/urit/urit/internal/session"
|
|
)
|
|
|
|
// sysopLibraryMenu is the library management sub-menu.
|
|
func sysopLibraryMenu(ctx *Context) {
|
|
ctx.Sess.NewLine()
|
|
ctx.Sess.Color(session.AnsiFgYellow, session.AnsiBold)
|
|
ctx.Sess.WriteString("Library Management\r\n")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
ctx.Sess.WriteString(strings.Repeat("─", 40) + "\r\n")
|
|
|
|
ctx.Sess.Color(session.AnsiFgBrightWhite)
|
|
ctx.Sess.WriteString(" [L] ")
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString("List libraries\r\n")
|
|
ctx.Sess.Color(session.AnsiFgBrightWhite)
|
|
ctx.Sess.WriteString(" [C] ")
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString("Create library\r\n")
|
|
ctx.Sess.Color(session.AnsiFgBrightWhite)
|
|
ctx.Sess.WriteString(" [E] ")
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString("Edit library\r\n")
|
|
ctx.Sess.Color(session.AnsiFgBrightWhite)
|
|
ctx.Sess.WriteString(" [D] ")
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString("Delete library\r\n")
|
|
ctx.Sess.Color(session.AnsiFgBrightWhite)
|
|
ctx.Sess.WriteString(" [Q] ")
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString("Return\r\n")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
|
|
for {
|
|
ctx.Sess.NewLine()
|
|
ctx.Sess.Color(session.AnsiFgYellow)
|
|
ctx.Sess.WriteString("Libraries> ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
|
|
ch, err := ctx.Sess.ReadKey(idleTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
switch toUpper(ch) {
|
|
case 'L':
|
|
ctx.Sess.WriteString("List\r\n")
|
|
sysopListLibraries(ctx)
|
|
case 'C':
|
|
ctx.Sess.WriteString("Create\r\n")
|
|
sysopCreateLibrary(ctx)
|
|
case 'E':
|
|
ctx.Sess.WriteString("Edit\r\n")
|
|
sysopEditLibraryPrompt(ctx)
|
|
case 'D':
|
|
ctx.Sess.WriteString("Delete\r\n")
|
|
sysopDeleteLibraryPrompt(ctx)
|
|
case 'Q', '\x1b':
|
|
ctx.Sess.WriteString("Return\r\n")
|
|
return
|
|
case '?':
|
|
ctx.Sess.WriteString("Help\r\n")
|
|
ctx.Sess.Color(session.AnsiFgBrightBlack)
|
|
ctx.Sess.WriteString(" L=List C=Create E=Edit D=Delete Q=Quit\r\n")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
}
|
|
}
|
|
}
|
|
|
|
// sysopListLibraries displays all libraries with their configuration.
|
|
func sysopListLibraries(ctx *Context) {
|
|
libs, err := ctx.Store.ListLibraries()
|
|
if err != nil {
|
|
ctx.Sess.WriteString(" Error loading libraries.\r\n")
|
|
return
|
|
}
|
|
|
|
ctx.Sess.NewLine()
|
|
ctx.Sess.Color(session.AnsiFgBrightBlack)
|
|
ctx.Sess.Printf(" %-4s %-16s %5s %-11s %-11s %s\r\n",
|
|
"ID", "Name", "Files", "Upload", "Download", "Max")
|
|
ctx.Sess.WriteString(strings.Repeat("─", 68) + "\r\n")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
|
|
if len(libs) == 0 {
|
|
ctx.Sess.WriteString(" (no libraries)\r\n")
|
|
return
|
|
}
|
|
|
|
for _, l := range libs {
|
|
ctx.Sess.Printf(" %-4d %-16s %5d %3d — %-3d %3d — %-3d %d\r\n",
|
|
l.ID, truncate(l.Name, 16), l.FileCount,
|
|
l.UploadLow, l.UploadHigh,
|
|
l.DownloadLow, l.DownloadHigh, l.MaxFiles)
|
|
}
|
|
|
|
ctx.Sess.NewLine()
|
|
ctx.Sess.Color(session.AnsiFgBrightBlack)
|
|
ctx.Sess.Printf(" %d library(ies)\r\n", len(libs))
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
}
|
|
|
|
// sysopCreateLibrary walks through creating a new library.
|
|
func sysopCreateLibrary(ctx *Context) {
|
|
ctx.Sess.NewLine()
|
|
ctx.Sess.Color(session.AnsiFgYellow, session.AnsiBold)
|
|
ctx.Sess.WriteString("Create Library\r\n")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
ctx.Sess.WriteString(strings.Repeat("─", 30) + "\r\n")
|
|
|
|
// Name
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString(" Name: ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
name, err := ctx.Sess.ReadLine("", 30, inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
ctx.Sess.WriteString(" Cancelled.\r\n")
|
|
return
|
|
}
|
|
|
|
// File path — directory where uploaded files will be stored.
|
|
// Default to a subdirectory next to the database file.
|
|
dataDir := filepath.Dir(ctx.Cfg.Storage.SQLitePath) + "/"
|
|
defaultPath := dataDir +
|
|
strings.ToLower(strings.ReplaceAll(name, " ", "-")) + "/"
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.Printf(" File path [%s]: ", defaultPath)
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
filePath, err := ctx.Sess.ReadLine("", 80, inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
filePath = strings.TrimSpace(filePath)
|
|
if filePath == "" {
|
|
filePath = defaultPath
|
|
}
|
|
|
|
// Upload access range
|
|
uploadLow, uploadHigh := sysopReadRange(ctx, "Upload access", 1, 255)
|
|
|
|
// Download access range
|
|
downloadLow, downloadHigh := sysopReadRange(ctx, "Download access", 0, 255)
|
|
|
|
// Max files
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString(" Max files [200]: ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
maxStr, err := ctx.Sess.ReadLine("", 6, inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
maxFiles := 200
|
|
if s := strings.TrimSpace(maxStr); s != "" {
|
|
if v, err := strconv.Atoi(s); err == nil && v > 0 && v <= 10000 {
|
|
maxFiles = v
|
|
}
|
|
}
|
|
|
|
lib := &models.Library{
|
|
Name: name,
|
|
FilePath: filePath,
|
|
UploadLow: uploadLow,
|
|
UploadHigh: uploadHigh,
|
|
DownloadLow: downloadLow,
|
|
DownloadHigh: downloadHigh,
|
|
MaxFiles: maxFiles,
|
|
}
|
|
|
|
if err := ctx.Store.CreateLibrary(lib); err != nil {
|
|
ctx.Sess.Color(session.AnsiFgRed)
|
|
ctx.Sess.Printf(" Error: %v\r\n", err)
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
return
|
|
}
|
|
|
|
ctx.Sess.NewLine()
|
|
ctx.Sess.Color(session.AnsiFgGreen)
|
|
ctx.Sess.Printf(" Library created: %s (ID #%d)\r\n", lib.Name, lib.ID)
|
|
ctx.Sess.Color(session.AnsiFgBrightBlack)
|
|
ctx.Sess.Printf(" Path: %s\r\n", lib.FilePath)
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
}
|
|
|
|
// sysopEditLibraryPrompt asks for a library ID and enters the edit loop.
|
|
func sysopEditLibraryPrompt(ctx *Context) {
|
|
sysopListLibraries(ctx)
|
|
|
|
ctx.Sess.NewLine()
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString(" Library ID: ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
|
|
input, err := ctx.Sess.ReadLine("", 10, inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
input = strings.TrimSpace(input)
|
|
if input == "" {
|
|
return
|
|
}
|
|
|
|
id, err := strconv.ParseInt(input, 10, 64)
|
|
if err != nil || id < 1 {
|
|
ctx.Sess.WriteString(" Invalid ID.\r\n")
|
|
return
|
|
}
|
|
|
|
sysopEditLibrary(ctx, id)
|
|
}
|
|
|
|
// sysopEditLibrary is the edit loop for a single library.
|
|
func sysopEditLibrary(ctx *Context, id int64) {
|
|
lib, err := ctx.Store.GetLibrary(id)
|
|
if err != nil || lib == nil {
|
|
ctx.Sess.WriteString(" Library not found.\r\n")
|
|
return
|
|
}
|
|
|
|
dirty := false
|
|
|
|
for {
|
|
sysopDisplayLibrary(ctx, lib, dirty)
|
|
|
|
ch, err := ctx.Sess.ReadKey(idleTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
switch toUpper(ch) {
|
|
case '1': // Save
|
|
ctx.Sess.WriteString("Save\r\n")
|
|
if err := ctx.Store.UpdateLibrary(lib); err != nil {
|
|
ctx.Sess.Color(session.AnsiFgRed)
|
|
ctx.Sess.Printf(" Error: %v\r\n", err)
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
ctx.Sess.ReadKey(idleTimeout)
|
|
continue
|
|
}
|
|
ctx.Sess.Color(session.AnsiFgGreen)
|
|
ctx.Sess.Printf(" Library #%d saved.\r\n", lib.ID)
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
ctx.Sess.ReadKey(idleTimeout)
|
|
return
|
|
|
|
case '\x1b': // ESC — Cancel
|
|
ctx.Sess.WriteString("Cancel\r\n")
|
|
if dirty {
|
|
ctx.Sess.Color(session.AnsiFgYellow)
|
|
ctx.Sess.WriteString(" Unsaved changes! Discard? [Y/N] ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
confirm, err := ctx.Sess.ReadKey(inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if toUpper(confirm) != 'Y' {
|
|
ctx.Sess.WriteString("No\r\n")
|
|
continue
|
|
}
|
|
ctx.Sess.WriteString("Yes\r\n")
|
|
}
|
|
return
|
|
|
|
case 'A': // Name
|
|
ctx.Sess.WriteString("Name\r\n")
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString(" New name: ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
name, err := ctx.Sess.ReadLine(lib.Name, 30, inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
if name != "" && name != lib.Name {
|
|
lib.Name = name
|
|
dirty = true
|
|
}
|
|
|
|
case 'B': // FilePath
|
|
ctx.Sess.WriteString("Path\r\n")
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString(" File path: ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
fp, err := ctx.Sess.ReadLine(lib.FilePath, 80, inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
fp = strings.TrimSpace(fp)
|
|
if fp != "" && fp != lib.FilePath {
|
|
lib.FilePath = fp
|
|
dirty = true
|
|
}
|
|
|
|
case 'C': // UploadLow
|
|
ctx.Sess.WriteString("Upload Low\r\n")
|
|
if v, ok := sysopReadInt(ctx, "UploadLow", lib.UploadLow, 0, 255); ok {
|
|
lib.UploadLow = v
|
|
dirty = true
|
|
}
|
|
case 'D': // UploadHigh
|
|
ctx.Sess.WriteString("Upload High\r\n")
|
|
if v, ok := sysopReadInt(ctx, "UploadHigh", lib.UploadHigh, 0, 255); ok {
|
|
lib.UploadHigh = v
|
|
dirty = true
|
|
}
|
|
case 'E': // DownloadLow
|
|
ctx.Sess.WriteString("Download Low\r\n")
|
|
if v, ok := sysopReadInt(ctx, "DownloadLow", lib.DownloadLow, 0, 255); ok {
|
|
lib.DownloadLow = v
|
|
dirty = true
|
|
}
|
|
case 'F': // DownloadHigh
|
|
ctx.Sess.WriteString("Download High\r\n")
|
|
if v, ok := sysopReadInt(ctx, "DownloadHigh", lib.DownloadHigh, 0, 255); ok {
|
|
lib.DownloadHigh = v
|
|
dirty = true
|
|
}
|
|
case 'G': // MaxFiles
|
|
ctx.Sess.WriteString("Max Files\r\n")
|
|
if v, ok := sysopReadInt(ctx, "MaxFiles", lib.MaxFiles, 1, 10000); ok {
|
|
lib.MaxFiles = v
|
|
dirty = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sysopDisplayLibrary renders the library detail screen for editing.
|
|
func sysopDisplayLibrary(ctx *Context, lib *models.Library, dirty bool) {
|
|
ctx.Sess.ClearScreen()
|
|
|
|
ctx.Sess.Color(session.AnsiFgYellow, session.AnsiBold)
|
|
ctx.Sess.Printf(" Library #%d", lib.ID)
|
|
if dirty {
|
|
ctx.Sess.Color(session.AnsiFgYellow)
|
|
ctx.Sess.WriteString(" *")
|
|
}
|
|
ctx.Sess.WriteString("\r\n")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
ctx.Sess.WriteString(strings.Repeat("─", 50) + "\r\n")
|
|
ctx.Sess.NewLine()
|
|
|
|
sysopField(ctx, "A", "Name", lib.Name)
|
|
sysopField(ctx, "B", "File Path", lib.FilePath)
|
|
ctx.Sess.NewLine()
|
|
|
|
// Upload access range
|
|
sysopFieldInt(ctx, "C", "Upload Low", lib.UploadLow)
|
|
sysopFieldInt(ctx, "D", "Upload High", lib.UploadHigh)
|
|
ctx.Sess.NewLine()
|
|
|
|
// Download access range
|
|
sysopFieldInt(ctx, "E", "Download Low", lib.DownloadLow)
|
|
sysopFieldInt(ctx, "F", "Download High", lib.DownloadHigh)
|
|
ctx.Sess.NewLine()
|
|
|
|
// Capacity and usage
|
|
sysopFieldInt(ctx, "G", "Max Files", lib.MaxFiles)
|
|
sysopField(ctx, " ", "Current Files",
|
|
fmt.Sprintf("%d", lib.FileCount))
|
|
ctx.Sess.NewLine()
|
|
|
|
// Metadata
|
|
if lib.LatestFile != nil {
|
|
sysopField(ctx, " ", "Latest File",
|
|
lib.LatestFile.Format("Jan 02, 2006 3:04 PM"))
|
|
} else {
|
|
sysopField(ctx, " ", "Latest File", "none")
|
|
}
|
|
sysopField(ctx, " ", "Created",
|
|
lib.CreatedAt.Format("Jan 02, 2006 3:04 PM"))
|
|
ctx.Sess.NewLine()
|
|
|
|
// Access summary
|
|
ctx.Sess.Color(session.AnsiFgBrightBlack)
|
|
ctx.Sess.Printf(" Upload: %s Download: %s\r\n",
|
|
describeAccess(lib.UploadLow, lib.UploadHigh),
|
|
describeAccess(lib.DownloadLow, lib.DownloadHigh))
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
ctx.Sess.NewLine()
|
|
|
|
// Footer
|
|
ctx.Sess.Color(session.AnsiFgBrightBlack)
|
|
ctx.Sess.WriteString(" 1=Save ESC=Cancel A-G=Edit fields\r\n")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
}
|
|
|
|
// sysopDeleteLibraryPrompt asks for a library ID and deletes it with confirmation.
|
|
func sysopDeleteLibraryPrompt(ctx *Context) {
|
|
sysopListLibraries(ctx)
|
|
|
|
ctx.Sess.NewLine()
|
|
ctx.Sess.Color(session.AnsiFgCyan)
|
|
ctx.Sess.WriteString(" Delete library ID: ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
|
|
input, err := ctx.Sess.ReadLine("", 10, inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
input = strings.TrimSpace(input)
|
|
if input == "" {
|
|
return
|
|
}
|
|
|
|
id, err := strconv.ParseInt(input, 10, 64)
|
|
if err != nil || id < 1 {
|
|
ctx.Sess.WriteString(" Invalid ID.\r\n")
|
|
return
|
|
}
|
|
|
|
lib, err := ctx.Store.GetLibrary(id)
|
|
if err != nil || lib == nil {
|
|
ctx.Sess.WriteString(" Library not found.\r\n")
|
|
return
|
|
}
|
|
|
|
fileCount, _ := ctx.Store.CountLibraryFiles(lib.ID)
|
|
|
|
ctx.Sess.Color(session.AnsiFgRed, session.AnsiBold)
|
|
ctx.Sess.Printf("\r\n Delete library: %s (#%d)\r\n", lib.Name, lib.ID)
|
|
ctx.Sess.Color(session.AnsiFgRed)
|
|
if fileCount > 0 {
|
|
ctx.Sess.Printf(" This will remove %d file record(s) from the database.\r\n", fileCount)
|
|
ctx.Sess.WriteString(" Actual files on disk will NOT be deleted.\r\n")
|
|
}
|
|
ctx.Sess.WriteString(" Are you sure? [Y/N] ")
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
|
|
confirm, err := ctx.Sess.ReadKey(inputTimeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if toUpper(confirm) != 'Y' {
|
|
ctx.Sess.WriteString("No\r\n")
|
|
ctx.Sess.WriteString(" Cancelled.\r\n")
|
|
return
|
|
}
|
|
ctx.Sess.WriteString("Yes\r\n")
|
|
|
|
if err := ctx.Store.DeleteLibrary(lib.ID); err != nil {
|
|
ctx.Sess.Color(session.AnsiFgRed)
|
|
ctx.Sess.Printf(" Error: %v\r\n", err)
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
return
|
|
}
|
|
|
|
ctx.Sess.Color(session.AnsiFgGreen)
|
|
ctx.Sess.Printf(" Library %s deleted.\r\n", lib.Name)
|
|
if lib.FilePath != "" {
|
|
ctx.Sess.Color(session.AnsiFgBrightBlack)
|
|
ctx.Sess.Printf(" Files in %s were not removed from disk.\r\n", lib.FilePath)
|
|
}
|
|
ctx.Sess.Color(session.AnsiReset)
|
|
}
|