├── trash.go ├── .gitignore ├── recycle.h ├── README.md ├── trash_windows.go ├── LICENSE ├── trash_test.go ├── trash_unix.go ├── recycle.c └── trash_osx.go /trash.go: -------------------------------------------------------------------------------- 1 | package trash 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | third_party/ 2 | ._* 3 | -------------------------------------------------------------------------------- /recycle.h: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | int RecycleFiles (char** filenames, int nFiles, int bConfirmed); 4 | char** makeCharArray(int size); 5 | void setArrayString(char **a, char *s, int n); 6 | void freeCharArray(char **a, int size); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-trash - crossplatform package to move files to trash 2 | 3 | go-trash is a cross-platform package to move files to trash. OSX, Windows and Linux (tested on Ubuntu) are supported. 4 | 5 | # Installing 6 | 7 | go get https://github.com/laurent22/go-trash 8 | 9 | # Building 10 | 11 | - On Windows, it builds successfully with MinGW, in MSYS shell. 12 | 13 | - On OSX and Linux, it should build as is. 14 | 15 | # Documentation 16 | 17 | http://godoc.org/github.com/laurent22/go-trash 18 | 19 | # License 20 | 21 | MIT 22 | -------------------------------------------------------------------------------- /trash_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package trash 4 | 5 | /* 6 | #include "recycle.h" 7 | */ 8 | import "C" 9 | 10 | import ( 11 | "errors" 12 | ) 13 | 14 | // Tells whether it is possible to move a file to the trash 15 | func IsAvailable() bool { 16 | return true 17 | } 18 | 19 | // Move the given file to the trash 20 | // filePath must be an absolute path 21 | func MoveToTrash(filePath string) (string, error) { 22 | files := []string{filePath} 23 | C_files := C.makeCharArray(C.int(len(files))) 24 | defer C.freeCharArray(C_files, C.int(len(files))) 25 | for i, s := range files { 26 | C.setArrayString(C_files, C.CString(s), C.int(i)) 27 | } 28 | 29 | success := C.RecycleFiles(C_files, C.int(len(files)), C.int(0)) 30 | if success != 1 { 31 | return "", errors.New("file could not be recycled") 32 | } 33 | return "", nil 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025-2025 Laurent Cozic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /trash_test.go: -------------------------------------------------------------------------------- 1 | package trash 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func testDir() string { 11 | pwd, err := os.Getwd() 12 | if err != nil { 13 | panic(err) 14 | } 15 | return filepath.Join(pwd, "testdir") 16 | } 17 | 18 | func setup(t *testing.T) { 19 | testDir := testDir() 20 | err := os.MkdirAll(testDir, 0700) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | } 25 | 26 | func teardown(t *testing.T) { 27 | testDir := testDir() 28 | os.RemoveAll(testDir) 29 | } 30 | 31 | func touch(filePath string) { 32 | ioutil.WriteFile(filePath, []byte(""), 0700) 33 | } 34 | 35 | func test_fileExists(filePath string) bool { 36 | _, err := os.Stat(filePath) 37 | return err == nil 38 | } 39 | 40 | func Test_MoveToTrash(t *testing.T) { 41 | setup(t) 42 | defer teardown(t) 43 | 44 | filePath := testDir() + "/go-trash-test-file" 45 | touch(filePath) 46 | 47 | if !test_fileExists(filePath) { 48 | t.Fatal("Could not create test file") 49 | } 50 | 51 | _, err := MoveToTrash(filePath) 52 | if err != nil { 53 | t.Errorf("Expected no error, got %s", err) 54 | } 55 | 56 | if test_fileExists(filePath) { 57 | t.Error("File was not deleted") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /trash_unix.go: -------------------------------------------------------------------------------- 1 | // +build linux freebsd 2 | 3 | package trash 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | var isAvailable_ int = -1 11 | var toolName_ string 12 | 13 | // Tells whether it is possible to move a file to the trash 14 | func IsAvailable() bool { 15 | if isAvailable_ < 0 { 16 | toolName_ = "" 17 | isAvailable_ = 0 18 | 19 | candidates := []string{ 20 | "gvfs-trash", 21 | "trash", 22 | } 23 | 24 | for _, candidate := range candidates { 25 | err := exec.Command("type", candidate).Run() 26 | ok := false 27 | if err == nil { 28 | ok = true 29 | } else { 30 | err = exec.Command("sh", "-c", "type "+candidate).Run() 31 | if err == nil { 32 | ok = true 33 | } 34 | } 35 | 36 | if ok { 37 | toolName_ = candidate 38 | isAvailable_ = 1 39 | return true 40 | } 41 | } 42 | 43 | return false 44 | } else if isAvailable_ == 1 { 45 | return true 46 | } 47 | 48 | return false 49 | } 50 | 51 | // Move the given file to the trash 52 | // filePath must be an absolute path 53 | func MoveToTrash(filePath string) (string, error) { 54 | if IsAvailable() { 55 | err := exec.Command(toolName_, filePath).Run() 56 | return "", err 57 | } 58 | 59 | os.Remove(filePath) 60 | return "", nil 61 | } 62 | -------------------------------------------------------------------------------- /recycle.c: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | // Slightly modified version of recycle.c from Matt Ginzton - http://www.maddogsw.com/cmdutils/ 4 | // Mainly replaced BOOL type by int to make it easier to call the function from Go. 5 | // 6 | // Also added functions for conversion between Go []string and **char from John Barham: 7 | // https://groups.google.com/forum/#!topic/golang-nuts/pQueMFdY0mk 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | char **makeCharArray(int size) { 14 | return calloc(sizeof(char*), size); 15 | } 16 | 17 | void setArrayString(char **a, char *s, int n) { 18 | a[n] = s; 19 | } 20 | 21 | void freeCharArray(char **a, int size) { 22 | int i; 23 | for (i = 0; i < size; i++) 24 | free(a[i]); 25 | free(a); 26 | } 27 | 28 | int RecycleFiles (char** filenames, int nFiles, int bConfirmed) 29 | { 30 | SHFILEOPSTRUCT opRecycle; 31 | char* pszFilesToRecycle; 32 | char* pszNext; 33 | int i, len; 34 | int success = 1; 35 | char szLongBuf[MAX_PATH]; 36 | char* lastComponent; 37 | 38 | //fill filenames to delete 39 | len = 0; 40 | for (i = 0; i < nFiles; i++) 41 | { 42 | GetFullPathName (filenames[i], sizeof(szLongBuf), szLongBuf, &lastComponent); 43 | len += lstrlen (szLongBuf) + 1; 44 | } 45 | 46 | pszFilesToRecycle = malloc (len + 1); 47 | pszNext = pszFilesToRecycle; 48 | for (i = 0; i < nFiles; i++) 49 | { 50 | GetFullPathName (filenames[i], sizeof(szLongBuf), szLongBuf, &lastComponent); 51 | 52 | lstrcpy (pszNext, szLongBuf); 53 | pszNext += lstrlen (pszNext) + 1; //advance past terminator 54 | } 55 | *pszNext = 0; //double-terminate 56 | 57 | //fill fileop structure 58 | opRecycle.hwnd = NULL; 59 | opRecycle.wFunc = FO_DELETE; 60 | opRecycle.pFrom = pszFilesToRecycle; 61 | opRecycle.pTo = "\0\0"; 62 | opRecycle.fFlags = FOF_ALLOWUNDO; 63 | if (bConfirmed) 64 | opRecycle.fFlags |= FOF_NOCONFIRMATION; 65 | opRecycle.lpszProgressTitle = "Recycling files..."; 66 | 67 | if (0 != SHFileOperation (&opRecycle)) 68 | success = 0; 69 | if (opRecycle.fAnyOperationsAborted) 70 | success = 0; 71 | 72 | free (pszFilesToRecycle); 73 | 74 | return success; 75 | } -------------------------------------------------------------------------------- /trash_osx.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package trash 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | "os/user" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // Adapted from https://github.com/morgant/tools-osx/blob/master/src/trash 19 | func haveScriptableFinder() (bool, error) { 20 | // Get current user 21 | user, err := user.Current() 22 | if err != nil { 23 | return false, err 24 | } 25 | 26 | // Get processes for current user 27 | cmd := exec.Command("ps", "-u", user.Username) 28 | output, err := cmd.Output() 29 | if err != nil { 30 | return false, err 31 | } 32 | 33 | // Find Finder process ID, if it is running 34 | finderPid := 0 35 | lines := strings.Split(string(output), "\n") 36 | for _, line := range lines { 37 | if strings.Index(line, "CoreServices/Finder.app") >= 0 { 38 | splitted := strings.Split(line, " ") 39 | index := 0 40 | for _, token := range splitted { 41 | if token == " " || token == "" { 42 | continue 43 | } 44 | index++ 45 | if index == 2 { 46 | finderPid, err = strconv.Atoi(token) 47 | if err != nil { 48 | return false, err 49 | } 50 | break 51 | } 52 | } 53 | } 54 | } 55 | 56 | if finderPid <= 0 { 57 | return false, errors.New("could not find Finder process ID") 58 | } 59 | 60 | // TODO: test with screen 61 | if os.Getenv("STY") != "" { 62 | return false, errors.New("currently running in screen") 63 | } 64 | 65 | return true, nil 66 | } 67 | 68 | // filePath must be an absolute path 69 | func pathVolume(filePath string) string { 70 | pieces := strings.Split(filePath[1:], "/") 71 | if len(pieces) <= 2 { 72 | return "" 73 | } 74 | if pieces[0] != "Volumes" { 75 | return "" 76 | } 77 | volumeName := pieces[1] 78 | cmd := exec.Command("readlink", "/Volumes/"+volumeName) 79 | output, _ := cmd.Output() 80 | if strings.Trim(string(output), " \t\r\n") == "/" { 81 | return "" 82 | } 83 | return volumeName 84 | } 85 | 86 | func fileTrashPath(filePath string) (string, error) { 87 | volumeName := pathVolume(filePath) 88 | trashPath := "" 89 | if volumeName != "" { 90 | trashPath = fmt.Sprintf("/Volumes/%s/.Trashes/%d", volumeName, os.Getuid()) 91 | } else { 92 | user, err := user.Current() 93 | if err != nil { 94 | return "", err 95 | } 96 | trashPath = fmt.Sprintf("/Users/%s/.Trash", user.Username) 97 | } 98 | return trashPath, nil 99 | } 100 | 101 | func fileExists(filePath string) bool { 102 | _, err := os.Stat(filePath) 103 | return err == nil 104 | } 105 | 106 | // Tells whether it is possible to move a file to the trash 107 | func IsAvailable() bool { 108 | return true 109 | } 110 | 111 | // Move the given file to the trash 112 | // filePath must be an absolute path 113 | func MoveToTrash(filePath string) (string, error) { 114 | if !fileExists(filePath) { 115 | return "", errors.New("file does not exist or is not accessible") 116 | } 117 | 118 | ok, err := haveScriptableFinder() 119 | 120 | if ok { 121 | // Do this in a loop because Finder sometime randomly fails with this error: 122 | // 29:106: execution error: Finder got an error: Handler can’t handle objects of this class. (-10010) 123 | // Repeating the operation usually fixes the issue. 124 | maxLoop := 3 125 | for i := 0; i < maxLoop; i++ { 126 | time.Sleep(time.Duration(i*500) * time.Millisecond) 127 | 128 | cmd := exec.Command("/usr/bin/osascript", "-e", "tell application \"Finder\" to delete POSIX file \""+filePath+"\"") 129 | var stdout bytes.Buffer 130 | cmd.Stdout = &stdout 131 | var stderr bytes.Buffer 132 | cmd.Stderr = &stderr 133 | err := cmd.Run() 134 | 135 | if err != nil { 136 | err = errors.New(fmt.Sprintf("%s: %s %s", err, stdout.String(), stderr.String())) 137 | } 138 | 139 | if stderr.Len() > 0 { 140 | err = errors.New(fmt.Sprintf("%s, %s", stdout.String(), stderr.String())) 141 | } 142 | 143 | if err != nil { 144 | if i >= maxLoop-1 { 145 | return "", err 146 | } else { 147 | continue 148 | } 149 | } 150 | 151 | break 152 | } 153 | } else { 154 | return "", errors.New(fmt.Sprintf("scriptable Finder not available: %s", err)) 155 | 156 | // TODO: maybe based on https://github.com/morgant/tools-osx/blob/master/src/trash, move 157 | // the file to trash manually. Problem is that it won't be possible to restore the files 158 | // directly from the trash. 159 | 160 | // volumeName := pathVolume(filePath) 161 | // trashPath := "" 162 | // if volumeName != "" { 163 | // trashPath = fmt.Sprintf("/Volumes/%s/.Trashes/%d", volumeName, os.Getuid()) 164 | // } else { 165 | // user, err := user.Current() 166 | // if err != nil { 167 | // return err 168 | // } 169 | // trashPath = fmt.Sprintf("/Users/%s/.Trash", user.Username) 170 | // } 171 | // err = os.MkdirAll(trashPath, 0700) 172 | // if err != nil { 173 | // return err 174 | // } 175 | } 176 | 177 | return "", nil 178 | 179 | // Code below is not working well 180 | 181 | trashPath, err := fileTrashPath(filePath) 182 | if err != nil { 183 | return "", err 184 | } 185 | 186 | filename := filepath.Base(filePath) 187 | ext := filepath.Ext(filePath) 188 | filenameNoExt := filename[0 : len(filename)-len(ext)] 189 | 190 | possibleFiles, err := filepath.Glob(trashPath + "/" + filenameNoExt + " ??.??.??" + ext) 191 | if err != nil { 192 | return "", err 193 | } 194 | 195 | latestFile := "" 196 | var latestTime int64 197 | for _, f := range possibleFiles { 198 | fileInfo, err := os.Stat(f) 199 | if err != nil { 200 | continue 201 | } 202 | modTime := fileInfo.ModTime().UnixNano() 203 | if modTime > latestTime { 204 | latestTime = modTime 205 | latestFile = f 206 | } 207 | } 208 | 209 | if latestFile == "" { 210 | return "", errors.New("could not find path of file in trash") 211 | } 212 | 213 | return latestFile, nil 214 | } 215 | --------------------------------------------------------------------------------