package main import ( "encoding/json" "flag" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) type config struct { PasswordCommand string PasswordCommandArgs []string User string DocspellURL string ArchiveDirectory string ImportDirectories []string } type FileExistsResult struct { Exists bool `json:"exists"` Items []DocspellItem `json:"items"` File string `json:"file"` } type DocspellItem struct { ID string `json:"id"` Name string `json:"name"` Direction string `json:"direction"` State string `json:"state"` Created int64 `json:"created"` ItemDate int64 `json:"item_date"` } func main() { fmt.Println("##################### START #####################") fmt.Println(" Docspell Consumedir Cleaner - v0.1 beta") fmt.Println(" by jacob1123") fmt.Println(" (based on work by totti4ever) ") fmt.Println("#################################################") fmt.Println() uploadMissing := flag.Bool("upload-missing", os.Getenv("DS_CC_UPLOAD_MISSING") == "true", "true, to upload files to docspell that do not yet exist there") flag.Parse() // Read config from user profile homeDir, err := os.UserHomeDir() if err != nil { fmt.Println("Error getting user home directory:", err) os.Exit(1) } configFile := filepath.Join(homeDir, "docspell-import.json") configData, err := os.ReadFile(configFile) if err != nil { fmt.Println("Error reading config file:", err) os.Exit(1) } var cfg config if err := json.Unmarshal(configData, &cfg); err != nil { fmt.Println("Error parsing config file:", err) os.Exit(1) } // Get password from command cmd := exec.Command(cfg.PasswordCommand, cfg.PasswordCommandArgs...) password, err := cmd.Output() if err != nil { fmt.Println("Error getting password:", err) os.Exit(1) } validateConfig(cfg) // Login loginCmd := exec.Command("dsc", "login", "--user", cfg.User, "--password", strings.TrimSpace(string(password))) if err := loginCmd.Run(); err != nil { fmt.Println("Login failed:", err) os.Exit(0) } // Check for jq if _, err := exec.LookPath("jq"); err != nil { fmt.Println("please install 'jq'") os.Exit(-4) } fmt.Println("Settings:") if *uploadMissing { fmt.Println(" - UPLOAD files? YES") fmt.Println(" files not existing in Docspell will be uploaded and will be re-checked in the next run.") } else { fmt.Println(" - UPLOAD files? no") fmt.Println(" files not existing in Docspell will NOT be uploaded and stay where they are.") } fmt.Println() fmt.Println() fmt.Println("Press 'ctrl+c' to cancel") time.Sleep(time.Second) fmt.Println() fmt.Println() for _, dsConsumedir := range cfg.ImportDirectories { fmt.Printf("Scanning folder '%s'\n", dsConsumedir) fmt.Println() fmt.Println() err = filepath.Walk(dsConsumedir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } fmt.Printf("Checking '%s'\n", path) // Check if file exists in Docspell cmd := exec.Command("dsc", "-f", "json", "file-exists", path) output, err := cmd.Output() if err != nil { fmt.Printf("ERROR %v\n", err) return nil } var fileExistsResponse []FileExistsResult if err := json.Unmarshal(output, &fileExistsResponse); err != nil { fmt.Printf("ERROR parsing response: %v\n", err) return nil } if fileExistsResponse[0].Exists { err := handleExistingFile(cfg, fileExistsResponse[0]) if err != nil { fmt.Println("ERROR", err) } } else { fmt.Println("Files does not exist, yet") if *uploadMissing { fmt.Print("...uploading file..") cmd = exec.Command("dsc", "-f", "json", "upload", path) output, err := cmd.Output() if err != nil { fmt.Printf("\nERROR uploading: %v\n", err) return nil } var uploadResult map[string]interface{} if err := json.Unmarshal(output, &uploadResult); err != nil { fmt.Printf("\nERROR parsing upload result: %v\n", err) return nil } if uploadResult["success"].(bool) { fmt.Println(". done") } else { fmt.Printf("\nERROR %v\n", uploadResult) } } } return nil }) if err != nil { fmt.Printf("Error walking directory: %v\n", err) os.Exit(1) } } fmt.Println("################# DONE #################") fmt.Println(time.Now().Format(time.RFC1123)) } func handleExistingFile(cfg config, fileExistsResponse FileExistsResult) error { // File exists in Docspell items := fileExistsResponse.Items item := items[0] itemID := item.ID itemName := item.Name // Get item details cmd := exec.Command("dsc", "-f", "json", "item", "get", itemID) output, err := cmd.Output() if err != nil { return fmt.Errorf("get item details: %w", err) } var itemDetails DocspellItemDetails if err := json.Unmarshal(output, &itemDetails); err != nil { return fmt.Errorf("pars item details: %w", err) } folder := "null" if itemDetails.Folder != nil { folder = itemDetails.Folder.Name } extension := filepath.Ext(fileExistsResponse.File)[1:] var corr string if itemDetails.CorrespondingOrganisation != nil && itemDetails.CorrespondingOrganisation.Name != "" { corr = itemDetails.CorrespondingOrganisation.Name } else if itemDetails.CorrespondingPerson != nil && itemDetails.CorrespondingPerson.Name != "" { corr = itemDetails.CorrespondingPerson.Name } fmt.Printf("File already exists: '%s @ %s/app/item/%s'\n", itemName, cfg.DocspellURL, itemID) state := item.State if state != "confirmed" { fmt.Println("... but is not confirmed yet - not doing anything.") return nil } itemDate := item.ItemDate if itemDate == 0 { fmt.Println("... but has no date - not doing anything.") return nil } date := time.Unix(itemDate/1000, 0) curDir := filepath.Join(cfg.ArchiveDirectory, folder, date.Format("2006/01")) if err := os.MkdirAll(curDir, 0755); err != nil { return fmt.Errorf("create directory: %w", err) } subfolder := fmt.Sprintf("%s %s - %s.%s", date.Format("20060102"), corr, itemName, extension) newPath := filepath.Join(curDir, subfolder) if err := os.Rename(fileExistsResponse.File, newPath); err != nil { return fmt.Errorf("move file: %w", err) } fmt.Printf("... moving to archive by date ('%s')\n", curDir) return nil } func validateConfig(cfg config) { if len(cfg.ImportDirectories) == 0 || cfg.ArchiveDirectory == "" { fmt.Println("FATAL Parameter missing") fmt.Printf(" import directories: %v\n", cfg.ImportDirectories) fmt.Printf(" archive directory: %s\n", cfg.ArchiveDirectory) os.Exit(-2) } if cfg.User == "" { fmt.Println("FATAL User is missing") os.Exit(-3) } } type DocspellItemDetails struct { DocspellItem CorrespondingOrganisation *DocspellEntity `json:"corr-org"` CorrespondingPerson *DocspellEntity `json:"corr-person"` Folder *DocspellEntity `json:"folder"` } type DocspellEntity struct { ID string `json:"id"` Name string `json:"name"` }