Files
opencloud/services/storage-users/pkg/command/uploads.go
T
Jörn Friedrich Dreyer 04ad5b84bb fix(storage-users): keep uploads sessions CLI out of the dcfs event queue
The uploads sessions command builds a posix driver with async file
uploads enabled, which makes it join the "dcfs" JetStream queue group.
Clean/restart/resume events published by the command itself can then be
routed to the CLI's own short-lived consumer; the nats client auto-acks
them and they are lost when the process exits, leaving stale upload
sessions behind.

Disable asyncfileuploads for the CLI driver instance so it only lists
sessions and publishes events. Also emit [] instead of null when the
session list is empty so the JSON output stays parseable.
2026-09-25 15:47:47 +02:00

321 lines
9.5 KiB
Go

package command
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/config"
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/config/parser"
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/event"
"github.com/opencloud-eu/opencloud/services/storage-users/pkg/revaconfig"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// Session contains the information of an upload session
type Session struct {
ID string `json:"id"`
Space string `json:"space"`
Filename string `json:"filename"`
Offset int64 `json:"offset"`
Size int64 `json:"size"`
Executant userpb.UserId `json:"executant"`
SpaceOwner *userpb.UserId `json:"spaceowner,omitempty"`
Expires time.Time `json:"expires"`
Processing bool `json:"processing"`
ScanDate time.Time `json:"virus_scan_date"`
ScanResult string `json:"virus_scan_result"`
Status string `json:"status"`
StatusMessage string `json:"status_message"`
}
// Uploads is the entry point for the uploads command
func Uploads(cfg *config.Config) *cobra.Command {
uploadsCmd := &cobra.Command{
Use: "uploads",
Short: "manage unfinished uploads",
}
uploadsCmd.AddCommand([]*cobra.Command{
ListUploadSessions(cfg),
}...)
return uploadsCmd
}
// ListUploadSessions prints a list of upload sessiens
func ListUploadSessions(cfg *config.Config) *cobra.Command {
listUploadSessionsCmd := &cobra.Command{
Use: "sessions",
Short: "Print a list of upload sessions",
PreRunE: func(cmd *cobra.Command, args []string) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
RunE: func(cmd *cobra.Command, args []string) error {
var err error
f, ok := registry.NewFuncs[cfg.Driver]
if !ok {
fmt.Fprintf(os.Stderr, "Unknown filesystem driver '%s'\n", cfg.Driver)
os.Exit(1)
}
drivers := revaconfig.StorageProviderDrivers(cfg)
var fsStream events.Stream
if cfg.Driver == "posix" {
// We need to init the posix driver with 'scanfs' disabled
posixDrivers := revaconfig.Posix(cfg, false, false)
// Disable async file uploads: this command only lists sessions and publishes
// events, but with async uploads enabled the driver would join the "dcfs"
// JetStream queue group. Events routed to this short-lived CLI member are
// auto-acked by the nats client and lost when the process exits, so the
// clean/restart/resume events published below would never reach the daemon.
posixDrivers["asyncfileuploads"] = false
drivers["posix"] = posixDrivers
// Also posix refuses to start without an events stream
fsStream, err = event.NewStream(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create event stream for posix driver: %v\n", err)
os.Exit(1)
}
}
fs, err := f(drivers[cfg.Driver].(map[string]any), fsStream, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to initialize filesystem driver '%s'\n", cfg.Driver)
return err
}
managingFS, ok := fs.(storage.UploadSessionLister)
if !ok {
fmt.Fprintf(os.Stderr, "'%s' storage does not support listing upload sessions\n", cfg.Driver)
os.Exit(1)
}
restart, _ := cmd.Flags().GetBool("restart")
resume, _ := cmd.Flags().GetBool("resume")
clean, _ := cmd.Flags().GetBool("clean")
renderJson, _ := cmd.Flags().GetBool("json")
var stream events.Stream
if restart || resume || clean {
stream, err = event.NewStream(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create event stream: %v\n", err)
os.Exit(1)
}
}
filter := buildFilter(cmd)
uploads, err := managingFS.ListUploadSessions(cmd.Context(), filter)
if err != nil {
return err
}
var table *tablewriter.Table
raw := []*Session{}
if !renderJson {
fmt.Println(buildInfo(filter))
table = tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
table.Header([]string{"Space", "Upload Id", "Name", "Status", "Message", "Offset", "Size", "Executant", "Owner", "Expires", "Scan Date", "Scan Result"})
}
for _, u := range uploads {
ref := u.Reference()
sr, sd := u.ScanData()
session := Session{
Space: ref.GetResourceId().GetSpaceId(),
ID: u.ID(),
Filename: u.Filename(),
Offset: u.Offset(),
Size: u.Size(),
Executant: u.Executant(),
SpaceOwner: u.SpaceOwner(),
Expires: u.Expires(),
ScanDate: sd,
ScanResult: sr,
Status: string(u.Status()),
StatusMessage: u.StatusMessage(),
}
if renderJson {
raw = append(raw, &session)
} else {
table.Append([]string{
session.Space,
session.ID,
session.Filename,
session.Status,
session.StatusMessage,
strconv.FormatInt(session.Offset, 10),
strconv.FormatInt(session.Size, 10),
session.Executant.OpaqueId,
session.SpaceOwner.GetOpaqueId(),
session.Expires.Format(time.RFC3339),
session.ScanDate.Format(time.RFC3339),
session.ScanResult,
})
}
switch {
case restart:
if err := events.Publish(context.Background(), stream, events.RestartPostprocessing{
UploadID: u.ID(),
Timestamp: utils.TSNow(),
}); err != nil {
fmt.Fprintf(os.Stderr, "Failed to send restart event for upload session '%s'\n", u.ID())
// if publishing fails there is no need to try publishing other events - they will fail too.
os.Exit(1)
}
case resume:
if err := events.Publish(context.Background(), stream, events.ResumePostprocessing{
UploadID: u.ID(),
Timestamp: utils.TSNow(),
}); err != nil {
fmt.Fprintf(os.Stderr, "Failed to send resume event for upload session '%s'\n", u.ID())
// if publishing fails there is no need to try publishing other events - they will fail too.
os.Exit(1)
}
case clean:
if err := events.Publish(context.Background(), stream, events.CleanUpload{
UploadID: u.ID(),
Timestamp: utils.TSNow(),
}); err != nil {
fmt.Fprintf(os.Stderr, "Failed to send clean upload event for upload session '%s'\n", u.ID())
// if publishing fails there is no need to try publishing other events - they will fail too.
os.Exit(1)
}
}
}
if !renderJson {
table.Render()
return nil
}
j, err := json.Marshal(raw)
if err != nil {
fmt.Println(err)
return err
}
fmt.Println(string(j))
return nil
},
}
listUploadSessionsCmd.Flags().String("id", "", "filter sessions by upload session id")
listUploadSessionsCmd.Flags().Bool("processing", false, "filter sessions by processing status")
listUploadSessionsCmd.Flags().Bool("expired", false, "filter sessions by expired status")
listUploadSessionsCmd.Flags().Bool("has-virus", false, "filter sessions by virus scan result")
listUploadSessionsCmd.Flags().Bool("json", false, "output as json")
listUploadSessionsCmd.Flags().Bool("restart", false, "send restart event for all listed sessions. Only one of resume/restart/clean can be set.")
listUploadSessionsCmd.Flags().Bool("resume", false, "send resume event for all listed sessions. Only one of resume/restart/clean can be set.")
listUploadSessionsCmd.Flags().Bool("clean", false, "remove uploads for all listed sessions. Only one of resume/restart/clean can be set.")
return listUploadSessionsCmd
}
func buildFilter(cmd *cobra.Command) storage.UploadSessionFilter {
filter := storage.UploadSessionFilter{}
if cmd.Flag("processing").Changed {
processingValue, _ := cmd.Flags().GetBool("processing")
filter.Processing = &processingValue
}
if cmd.Flag("expired").Changed {
expiredValue, _ := cmd.Flags().GetBool("expired")
filter.Expired = &expiredValue
}
if cmd.Flag("has-virus").Changed {
infectedValue, _ := cmd.Flags().GetBool("has-virus")
filter.HasVirus = &infectedValue
}
if cmd.Flag("id").Changed {
idValue, _ := cmd.Flags().GetString("id")
if idValue != "" {
filter.ID = &idValue
}
}
return filter
}
func buildInfo(filter storage.UploadSessionFilter) string {
var b strings.Builder
if filter.Processing != nil {
if !*filter.Processing {
b.WriteString("Not ")
}
if b.Len() == 0 {
b.WriteString("Processing")
} else {
b.WriteString("processing")
}
}
if filter.Expired != nil {
if b.Len() != 0 {
b.WriteString(", ")
}
if !*filter.Expired {
if b.Len() == 0 {
b.WriteString("Not ")
} else {
b.WriteString("not ")
}
}
if b.Len() == 0 {
b.WriteString("Expired")
} else {
b.WriteString("expired")
}
}
if filter.HasVirus != nil {
if b.Len() != 0 {
b.WriteString(", ")
}
if !*filter.HasVirus {
if b.Len() == 0 {
b.WriteString("Not ")
} else {
b.WriteString("not ")
}
}
if b.Len() == 0 {
b.WriteString("Virusinfected")
} else {
b.WriteString("virusinfected")
}
}
if b.Len() == 0 {
b.WriteString("Session")
} else {
b.WriteString(" session")
}
if filter.ID != nil {
b.WriteString(" with id '" + *filter.ID + "'")
} else {
// to make `session` plural
b.WriteString("s")
}
b.WriteString(":")
return b.String()
}