-
Notifications
You must be signed in to change notification settings - Fork 1
/
trashbox_darwin.go
346 lines (302 loc) · 8.46 KB
/
trashbox_darwin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
//go:build darwin
// +build darwin
/*
Copyright © 2024 Kei-K23 <[email protected]>
*/
package trashbox
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
type metadata struct {
OriginalPath string `json:"original_path"`
Filename string `json:"file_name"`
DeletedAt time.Time `json:"deleted_at"`
Size int64 `json:"size"`
}
var trashDir = filepath.Join(os.Getenv("HOME"), ".Trash")
const trashboxMetadataExt = ".trashbox.metadata.json"
// MoveToTrash moves the specified file or directory to the system's Trash directory.
// This function generates a metadata file in the Trash for potential recovery.
//
// Parameters:
//
// path (string): The path of the file or directory to be moved to Trash.
//
// Returns:
//
// error: Returns an error if the file cannot be moved to Trash or if there are issues
// in creating the metadata file. Returns nil if successful.
//
// Example usage:
//
// err := MoveToTrash("/path/to/file.txt")
// if err != nil {
// log.Fatalf("Failed to move file to Trash: %v", err)
// }
//
// Notes:
// - On success, a metadata file is created in the Trash directory that stores the
// original location of the deleted file. This enables the file to be put back
// using the PutBackFromTrash function.
// - The function is currently tailored for macOS systems.
func MoveToTrash(path string) error {
// Get the absolute file path of delete file
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
info, err := os.Stat(absPath)
if err != nil {
return err
}
// Get the trash file path to move to .Trash directory
trashPath := filepath.Join(trashDir, filepath.Base(path))
// Move the file to .Trash directory
err = os.Rename(absPath, trashPath)
if err != nil {
return err
}
// Create metadata file for recovery the deleted file
metadata := metadata{OriginalPath: absPath, DeletedAt: time.Now(), Size: info.Size(), Filename: info.Name()}
metadataPath := trashPath + trashboxMetadataExt
// Create metadata file in Trash bin
metadataFile, err := os.Create(metadataPath)
if err != nil {
return err
}
defer metadataFile.Close()
encoder := json.NewEncoder(metadataFile)
err = encoder.Encode(metadata)
if err != nil {
return err
}
// Process is success and return nill
return nil
}
// PutBackFromTrash restores a previously deleted file from the Trash to its original location.
// The original location is determined from the metadata generated when the file was moved to Trash.
//
// Parameters:
//
// path (string): The name of the file or directory to be restored from Trash.
//
// Returns:
//
// error: Returns an error if the file cannot be restored or if there are issues
// with reading the metadata file. Returns nil if successful.
//
// Example usage:
//
// err := PutBackFromTrash("file.txt")
// if err != nil {
// log.Fatalf("Failed to put back file from Trash: %v", err)
// }
//
// Notes:
// - The function depends on a metadata file (generated by MoveToTrash) being present
// in the Trash directory, which contains the original path.
// - On success, the metadata file is removed from the Trash.
func PutBackFromTrash(path string) error {
// Get the Trash box path and metadata path
trashPath := filepath.Join(trashDir, path)
metadataPath := trashPath + trashboxMetadataExt
// Open metadata file to get original file path
metadataFile, err := os.Open(metadataPath)
if err != nil {
return err
}
defer metadataFile.Close()
var metadata metadata
decoder := json.NewDecoder(metadataFile)
err = decoder.Decode(&metadata)
if err != nil {
return err
}
// Put back file to original path
err = os.Rename(trashPath, metadata.OriginalPath)
if err != nil {
return err
}
// Remove the metadata file
err = os.Remove(metadataPath)
if err != nil {
return err
}
// Process is success and return nill
return nil
}
// ListTrash shows a list of metadata that related to moved files by MoveToTrash.
//
// Notes:
// - ListTrash only shows the metadata of file that was moved by MoveToTrash.
//
// Returns:
//
// []metadata: Returns a slice of metadata struct
//
// error: Returns an error when reading and listing metadata file inside Trash.
//
// Example usage:
//
// files, err := ListTrash()
// if err != nil {
// log.Fatalf("Failed to list metadata of files: %v", err)
// }
//
// Notes:
// - The function depends on a metadata file (generated by MoveToTrash) being present
// in the Trash directory, which contains the original path.
func ListTrash() ([]metadata, error) {
var files []metadata
parts, err := listTrashUsingAppleScript()
if err != nil {
return nil, err
}
if len(parts) == 0 {
return nil, fmt.Errorf("empty trash bin")
}
entries := strings.Split(parts[0], ",")
for _, entry := range entries {
if strings.HasSuffix(entry, ".metadata.json") {
filePath := filepath.Join(trashDir, strings.TrimSpace(entry))
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
var metadata metadata
decoder := json.NewDecoder(file)
if err = decoder.Decode(&metadata); err != nil {
return nil, err
}
files = append(files, metadata)
}
}
return files, nil
}
func listTrashUsingAppleScript() ([]string, error) {
script := `tell application "Finder" to get the name of every file of trash`
cmd := exec.Command("osascript", "-e", script)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
// Split the output by new lines and convert each entry to a string
var files []string
for _, b := range bytes.Split(out.Bytes(), []byte{'\n'}) {
if len(b) > 0 { // Check to avoid adding empty entries
files = append(files, string(b))
}
}
return files, nil
}
// DeletePermanently delete file from Trash bin permanently.
//
// Notes:
// - DeletePermanently only delete that have metadata file.
//
// Returns:
//
// error: Returns an error when deleting file is not found or incorrect path.
//
// Example usage:
//
// err := DeletePermanently("/path/to/file.txt")
// if err != nil {
// log.Fatalf("Failed to delete file: %v", err)
// }
//
// Notes:
// - The function depends on a metadata file (generated by MoveToTrash) being present
// in the Trash directory, which contains the original path.
func DeletePermanently(path string) error {
trashPath := filepath.Join(trashDir, path)
metadataPath := trashPath + trashboxMetadataExt
if _, err := os.Stat(trashPath); os.ErrNotExist == err {
return fmt.Errorf("file not found to delete permanently")
}
// Remove actual file
err := os.Remove(trashPath)
if err != nil {
return err
}
// Remove metadata file
err = os.Remove(metadataPath)
if err != nil {
return err
}
return nil
}
// AutoCleanTrash delete file from Trash bin within specific duration or remove all files.
//
// Notes:
// - AutoCleanTrash only delete that have metadata file.
//
// Parameters:
//
// orderThanDays ...int: Duration for deleting files or leave is empty to delete all files.
//
// Returns:
//
// error: Returns an error when deleting file is not found or incorrect path.
//
// Example usage:
//
// err := AutoCleanTrash(1)
// if err != nil {
// log.Fatalf("Failed to delete file: %v", err)
// }
//
// Notes:
// - The function depends on a metadata file (generated by MoveToTrash) being present
// in the Trash directory, which contains the original path.
func AutoCleanTrash(orderThanDays ...int) error {
if len(orderThanDays) > 1 {
return fmt.Errorf("invalid function argument. AutoCleanTrash function take one argument or zero argument")
}
files, err := ListTrash()
if err != nil {
return err // Return the error instead of nil
}
var wg sync.WaitGroup
var mu sync.Mutex
var errors []error
for _, file := range files {
// Determine if the file should be deleted based on the given criteria
shouldDelete := false
if len(orderThanDays) == 0 {
shouldDelete = true
} else if time.Since(file.DeletedAt).Hours() > float64(orderThanDays[0]*24) {
shouldDelete = true
}
// If the condition is met, delete the file
if shouldDelete {
wg.Add(1)
go func(file metadata) {
defer wg.Done()
// Use the correct filename/path for deletion
err := DeletePermanently(file.Filename)
if err != nil {
mu.Lock()
errors = append(errors, err)
mu.Unlock()
}
}(file) // Pass file as a parameter to avoid race conditions
}
}
wg.Wait()
if len(errors) > 0 {
return fmt.Errorf("some files could not be deleted permanently: %v", errors)
}
return nil
}