[Go] ゴミ箱にファイルを移動するgo-trashを作ってみた

この記事はゴミ箱について得た知見[^1] [^2]を基に、ファイルをゴミ箱に移動する`go-trash`を作ってみました。

https://github.com/Redh00k-k/go-trash


# ゴミ箱にファイルやディレクトリを移動
## Linux
ゴミ箱に移動させたいファイルを`$HOME/.local/share/files`に移動させることと、`$HOME/.local/share/info`に`<移動させたいファイル名>.trashinfo`ファイルを用意する必要があるだけです。

気を付けないといけないのは、ファイルやディレクトリをゴミ箱に移動させる前に`$trash/info`に対応するファイルを作成しなければならないことです。

実装は以下の通りです。こちらはオプション等を一切考えずファイル/ディレクトリをゴミ箱に移動させる機能だけ実装してあります。
```go
package main

import (
	"fmt"
	"os"
	"os/user"
	"path/filepath"
	"strings"
	"time"
)

// ~/.local/share/Trash/info/
type Info struct {
	path         string
	deletionDate time.Time
}

// trashinfoの文字列作成
func convertTrashInfo(i Info) string {
	return fmt.Sprintf("[Trash Info]\nPath=%s\nDeletionDate=%s\n", i.path, i.deletionDate.UTC().Format(time.RFC3339))
}

func moveToTrashBox(path string) (err error) {
	filename := filepath.Base(path)
	abs, err := filepath.Abs(path)
	if err != nil {
		fmt.Errorf("Failure to get absolute representation of path: %s", err)
		return err
	}

	info := Info{abs, time.Now()}
	user, err := user.Current()
	if err != nil {
		fmt.Errorf("Failure to get user's home directory: %s", err)
		return err
	}

    // 絶対パスに変更
	trashBase := strings.Replace("~/.local/share/Trash", "~", user.HomeDir, 1)
	err = os.WriteFile(trashBase+"/info/"+filename+".trashinfo", []byte(convertTrashInfo(info)), os.ModePerm)
	if err != nil {
		fmt.Errorf("Failure to writeFile source file: %s", err)
		return err
	}

	err = os.Rename(path, trashBase+"/files/"+filename)
	if err != nil {
		fmt.Errorf("Failure to rename source file: %s", err)
		return err
	}

	return
}

func main() {
    // カレントディレクトリのtestをゴミ箱に移動
	err := moveToTrashBox("test")
	if err != nil {
		fmt.Println("go-trash: cannot move to trashbox: ", err)
	}
}
```

## Windows
Windowsの場合はSHFileOperationというWindowsAPIを叩く必要があります。
このAPIを実行するためにSHFILEOPSTRUCT構造体やいくつかの定数を定義してあります。

実装は以下の通りです。こちらもオプション等を一切考えずファイル/ディレクトリをゴミ箱に移動させる機能だけ実装してあります。
```go
package main

import (
	"fmt"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"
)

// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationw
var (
	modshell32           = syscall.NewLazyDLL("Shell32.dll")
	procSHFileOperationW = modshell32.NewProc("SHFileOperationW")
)

// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shfileopstructa
type SHFILEOPSTRUCT struct {
	Hwnd                 uintptr
	Func                 uint32
	From                 *uint16
	To                   *uint16
	Flags                uint16
	AnyOperationsAborted int32
	NameMappings         *byte
	ProgressTitle        *uint16
}

func _SHFileOperation(
	shFileOp *SHFILEOPSTRUCT,
) (r1 uintptr, err error) {
	r1, _, err = procSHFileOperationW.Call(
		uintptr(unsafe.Pointer(shFileOp)),
	)

	return
}

func moveToTrashBox(path string) (ret uintptr, err error) {
	// https://pinvoke.net/default.aspx/Enums/FileFuncFlags.html
	FO_DELETE := 0x3

	// https://www.pinvoke.net/default.aspx/Enums/FILEOP_FLAGS.html
	// https://groups.google.com/g/microsoft.public.vb.winapi/c/htqEx2zQjGo
	FOF_SILENT := 0x4
	FOF_NOCONFIRMATION := 0x10
	FOF_ALLOWUNDO := 0x40

	var fileOp SHFILEOPSTRUCT
	fileOp.Hwnd = uintptr(0)
	fileOp.Func = uint32(FO_DELETE)
	fileOp.From = windows.StringToUTF16Ptr(path)
	fileOp.Flags = uint16(FOF_SILENT | FOF_ALLOWUNDO | FOF_NOCONFIRMATION)

	ret, err = _SHFileOperation(&fileOp)
	return
}

func main() {
	// カレントディレクトリのtestをゴミ箱に移動
	ret, err := moveToTrashBox("test")
	if err != nil {
		fmt.Println("go-trash: cannot move to trashbox: ", ret, err)
	}
}

```


# 参考
[^1]: https://qiita.com/redh00k/items/1e56d8bfd9e4e011f61c
[^2]: https://qiita.com/redh00k/items/690c55cd4167582e4019