@@ -2,51 +2,72 @@ package main
22
33import (
44 "bufio"
5- "enex2paperless/internal/config"
6- "enex2paperless/internal/logging"
7- "enex2paperless/pkg/enex"
85 "fmt"
96 "log/slog"
107 "os"
118 "path/filepath"
129 "strings"
13- "sync"
10+
11+ "enex2paperless/internal/config"
12+ "enex2paperless/internal/logging"
13+ "enex2paperless/pkg/enex"
1414
1515 "github.com/spf13/cobra"
1616)
1717
18+ // CLI flag variables
19+ var (
20+ howMany int
21+ verbose bool
22+ nocolor bool
23+ outputfolder string
24+ tags []string
25+ useFilenameAsTag bool
26+ )
27+
1828func main () {
1929 // define root command
2030 rootCmd := & cobra.Command {
2131 Use : "enex2paperless [file path]" ,
2232 Short : "ENEX to Paperless-NGX parser" ,
2333 Long : `An ENEX file parser for Paperless-NGX. https://github.com/kevinzehnder/enex2paperless` ,
24- Args : cobra .MinimumNArgs (1 ),
25- PreRun : func (cmd * cobra.Command , args []string ) {
26- // this block will execute after flag parsing and before the main Run
34+ Args : cobra .ExactArgs (1 ),
35+ PreRunE : func (cmd * cobra.Command , args []string ) error {
36+ // validate concurrent workers
37+ if howMany < 1 {
38+ return fmt .Errorf ("concurrent workers must be at least 1, got %d" , howMany )
39+ }
40+
41+ // validate output folder if specified
42+ if outputfolder != "" {
43+ info , err := os .Stat (outputfolder )
44+ if err != nil {
45+ if os .IsNotExist (err ) {
46+ return fmt .Errorf ("output folder does not exist: %s" , outputfolder )
47+ }
48+ return fmt .Errorf ("cannot access output folder: %w" , err )
49+ }
50+ if ! info .IsDir () {
51+ return fmt .Errorf ("output folder is not a directory: %s" , outputfolder )
52+ }
53+ }
2754
28- // configure SLOG with the determined log level from verbose flag
29- verbose , err := cmd .Flags ().GetBool ("verbose" ) // Ensure to get the flag value correctly
30- if err != nil {
31- fmt .Println ("Error retrieving verbose flag:" , err )
32- os .Exit (1 )
55+ // validate input file exists
56+ if _ , err := os .Stat (args [0 ]); err != nil {
57+ if os .IsNotExist (err ) {
58+ return fmt .Errorf ("input file does not exist: %s" , args [0 ])
59+ }
60+ return fmt .Errorf ("cannot access input file: %w" , err )
3361 }
3462
35- // set log level
63+ // set log level based on verbose flag
3664 var logLevel slog.Level
3765 if verbose {
3866 logLevel = slog .LevelDebug
3967 } else {
4068 logLevel = slog .LevelInfo
4169 }
4270
43- // nocolor option
44- nocolor , err := cmd .Flags ().GetBool ("nocolor" )
45- if err != nil {
46- fmt .Println ("Error retrieving nocolor flag:" , err )
47- os .Exit (1 )
48- }
49-
5071 opts := & slog.HandlerOptions {
5172 Level : logLevel ,
5273 }
@@ -55,76 +76,25 @@ func main() {
5576 logger := slog .New (logging .NewHandler (opts , nocolor ))
5677 slog .SetDefault (logger )
5778
58- // handle configuration
59- settings , err := config .GetConfig ()
60- if err != nil {
61- slog .Error ("configuration error:" , "error" , err )
62- os .Exit (1 )
63- }
64- slog .Debug (fmt .Sprintf ("configuration: %v" , settings ))
65-
66- // add to configuration
67- outputfolder , err := cmd .Flags ().GetString ("outputfolder" )
68- if err != nil {
69- fmt .Println ("Error retrieving outputfolder flag:" , err )
70- os .Exit (1 )
71- }
72-
73- if outputfolder != "" {
74- config .SetOutputFolder (outputfolder )
75- }
76-
77- // Set additional tags if provided
78- tags , err := cmd .Flags ().GetStringSlice ("tags" )
79- if err != nil {
80- fmt .Println ("Error retrieving tag flag:" , err )
81- os .Exit (1 )
82- }
83-
84- useFilenameAsTag , err := cmd .Flags ().GetBool ("use-filename-tag" )
85- if err != nil {
86- fmt .Println ("Error retrieving tag flag:" , err )
87- os .Exit (1 )
88- }
89- if useFilenameAsTag {
90- // Extract filename without path and extension
91- baseName := filepath .Base (args [0 ])
92- tagName := strings .TrimSuffix (baseName , filepath .Ext (baseName ))
93- tags = append (tags , tagName )
94- }
95-
96- if len (tags ) > 0 {
97- config .SetAdditionalTags (tags )
98- }
99-
79+ return nil
10080 },
10181
10282 // run main function
10383 Run : importENEX ,
10484 }
10585
10686 // add flags
107- var howMany int
10887 rootCmd .PersistentFlags ().IntVarP (& howMany , "concurrent" , "c" , 1 , "Number of concurrent consumers" )
109-
110- var verbose bool
11188 rootCmd .PersistentFlags ().BoolVarP (& verbose , "verbose" , "v" , false , "Enable verbose logging" )
112-
113- var nocolor bool
11489 rootCmd .PersistentFlags ().BoolVarP (& nocolor , "nocolor" , "n" , false , "Disable colored output" )
115-
116- var outputfolder string
11790 rootCmd .PersistentFlags ().StringVarP (& outputfolder , "outputfolder" , "o" , "" , "Output attachements to this folder, NOT paperless." )
118-
119- rootCmd .PersistentFlags ().StringSliceP ("tags" , "t" , nil , "Additional tags to add to all documents." )
120-
121- var useFilenameAsTag bool
91+ rootCmd .PersistentFlags ().StringSliceVarP (& tags , "tags" , "t" , nil , "Additional tags to add to all documents." )
12292 rootCmd .PersistentFlags ().BoolVarP (& useFilenameAsTag , "use-filename-tag" , "T" , false , "Add the ENEX filename as tag to all documents." )
12393
12494 // run root command
12595 err := rootCmd .Execute ()
12696 if err != nil {
127- fmt . Println ( "Error executing command:" , err )
97+ // cobra prints error message, we just handle exit code
12898 os .Exit (1 )
12999 }
130100}
@@ -133,114 +103,46 @@ func importENEX(cmd *cobra.Command, args []string) {
133103 slog .Debug ("starting importENEX" )
134104 settings , _ := config .GetConfig ()
135105
136- if settings .OutputFolder != "" {
137- slog .Info (fmt .Sprintf ("Output to local storage is enabled. Target is: %v" , settings .OutputFolder ))
106+ // Apply flag overrides to config
107+ if outputfolder != "" {
108+ settings .OutputFolder = outputfolder
138109 }
139110
140- // determine how many concurrent uploaders we want
141- howMany , err := cmd .Flags ().GetInt ("concurrent" )
142- if err != nil {
143- slog .Error ("failed to read flag" , "error" , err )
144- os .Exit (1 )
111+ if useFilenameAsTag {
112+ baseName := filepath .Base (args [0 ])
113+ tagName := strings .TrimSuffix (baseName , filepath .Ext (baseName ))
114+ tags = append (tags , tagName )
145115 }
146-
147- // prepare input file with initialized channels
148- filePath := args [0 ]
149- inputFile := enex .NewEnexFile (filePath )
150-
151- // Failure Catcher
152- var failedNotes []enex.Note
153- go func () {
154- inputFile .FailedNoteCatcher (& failedNotes )
155- inputFile .FailedNoteSignal <- true
156- }()
157-
158- // Producer
159- go func () {
160- err := inputFile .ReadFromFile ()
161- if err != nil {
162- slog .Error ("failed to read from file" , "error" , err )
163- os .Exit (1 )
164- }
165- }()
166-
167- // Consumers
168- var wg sync.WaitGroup
169- wg .Add (howMany )
170-
171- for i := 0 ; i < howMany ; i ++ {
172- go func () {
173- err := inputFile .UploadFromNoteChannel (settings .OutputFolder )
174- if err != nil {
175- slog .Error ("failed to upload resources" , "error" , err )
176- os .Exit (1 )
177- }
178-
179- wg .Done ()
180- }()
116+ if len (tags ) > 0 {
117+ settings .AdditionalTags = tags
181118 }
182- slog .Debug ("waiting for Consumers (WaitGroup)" )
183- wg .Wait ()
184-
185- // close failedNoteChannel when consumers are done
186- close (inputFile .FailedNoteChannel )
187119
188- // wait for FailedNoteCatcher
189- slog .Debug ( "waiting for FailedNoteCatcher" )
190- <- inputFile . FailedNoteSignal
120+ if settings . OutputFolder != "" {
121+ slog .Info ( fmt . Sprintf ( "Output to local storage is enabled. Target is: %v" , settings . OutputFolder ) )
122+ }
191123
192- // log results
193- slog .Info ("ENEX processing done" ,
194- slog .Int ("numberOfNotes" , int (inputFile .NumNotes .Load ())),
195- slog .Int ("totalFiles" , int (inputFile .Uploads .Load ())),
196- )
124+ // Prepare input file with initialized channels
125+ filePath := args [0 ]
126+ inputFile := enex .NewEnexFile (filePath , settings )
127+
128+ // Process the ENEX file with retry prompts
129+ result , err := inputFile .Process (enex.ProcessOptions {
130+ ConcurrentWorkers : howMany ,
131+ OutputFolder : settings .OutputFolder ,
132+ RetryPromptFunc : func (failedCount int ) bool {
133+ // Prompt user whether to retry failed notes
134+ slog .Warn ("there have been errors, starting retry cycle" , "errors" , failedCount )
135+ PressKeyToContinue ()
136+ return true
137+ },
138+ })
197139
198- for {
199- // if we still have failedNotes in this iteration, keep going
200- if len (failedNotes ) == 0 {
201- break
140+ if err != nil {
141+ slog . Error ( "processing completed with errors" , "error" , err )
142+ if len (result . FailedNotes ) > 0 {
143+ slog . Error ( "some notes could not be processed" , "failedCount" , len ( result . FailedNotes ))
202144 }
203-
204- slog .Warn ("there have been errors, starting retry cycle" , "errors" , len (failedNotes ))
205- PressKeyToContinue ()
206-
207- // all failed notes are now in failedNotes slice
208- // push notes that failed this Cycle into failedThisCycle slice
209- failedThisCycle := []enex.Note {}
210-
211- // Create a fresh EnexFile for the retry - empty file path since we're not reading a file
212- inputFile = enex .NewEnexFile ("" )
213-
214- // this feeds the failedNotes slice into the failedNoteChannel
215- go func () {
216- inputFile .FailedNoteCatcher (& failedThisCycle )
217- inputFile .FailedNoteSignal <- true
218- }()
219-
220- // this feeds the failedNotes into the Retry Channel
221- go inputFile .RetryFeeder (& failedNotes )
222-
223- // this works on the retry channel
224- wg .Add (1 )
225- go func () {
226- err = inputFile .UploadFromNoteChannel (settings .OutputFolder )
227- if err != nil {
228- slog .Error ("failed to upload resources" , "error" , err )
229- os .Exit (1 )
230- }
231- wg .Done ()
232- }()
233- wg .Wait ()
234-
235- // when the uploader is done, we can close the failedNoteChannel
236- // to signal to the FailedNote Catcher that it can stop
237- close (inputFile .FailedNoteChannel )
238-
239- // then we wait for the FailedNoteCatcher to stop
240- <- inputFile .FailedNoteSignal
241-
242- // we move the notes that failed this cycle into the failedNotes variable
243- failedNotes = failedThisCycle
145+ os .Exit (1 )
244146 }
245147
246148 slog .Info ("all notes processed successfully" )
0 commit comments