commit 13848fb227ed9810552798d7d6296f159eb2c1dc Author: Jamie Banks Date: Tue Jun 9 11:14:49 2026 +0100 feat: :sparkles: initialize project structure with frontend dependencies and backend protocols diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..129d522 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/bin +node_modules +frontend/dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb36dfa --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# GoExplore + +GoExplore is a cross-platform desktop file manager built with Wails v2 and Vanilla JS. It provides a unified UI for browsing and managing files across multiple remote and local filesystems. + +## Requirements +- Go 1.26 +- Wails v2 CLI (`go install github.com/wailsapp/wails/v2/cmd/wails@latest`) +- Node.js & npm +- Platform-specific dependencies for Wails (e.g. `libgtk-3-dev`, `libwebkit2gtk-4.0-dev` on Linux) + +## Configuration +Configuration is stored in `~/.config/goexplore/config.yaml`. +All secrets (passwords, keys) are stored securely in the OS keychain using `go-keyring`. + +## Build Instructions +1. Run `wails build -tags webkit2_41` to compile the production binary. +2. The output will be located in the `build/bin/` directory. + +## Development +To run in development mode with live reload: +```bash +wails dev -tags webkit2_41 +``` diff --git a/app.go b/app.go new file mode 100644 index 0000000..0581bb2 --- /dev/null +++ b/app.go @@ -0,0 +1,297 @@ +package main + +import ( + "context" + "fmt" + "goexplore/internal/config" + "goexplore/internal/explorer" + "goexplore/internal/keychain" + "goexplore/internal/protocols/local" + "goexplore/internal/protocols/nfs" + "goexplore/internal/protocols/s3" + "goexplore/internal/protocols/sftp" + "goexplore/internal/protocols/smb" + "goexplore/internal/protocols/webdav" + "goexplore/internal/transfer" + "os" + "path/filepath" + + "github.com/google/uuid" + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +type App struct { + ctx context.Context + cfg *config.Config + transferManager *transfer.Manager +} + +func NewApp() *App { + cfg, err := config.LoadConfig() + if err != nil { + fmt.Printf("Failed to load config: %v\n", err) + } + if cfg == nil { + cfg = &config.Config{Connections: []config.ConnectionConfig{}} + } + return &App{ + cfg: cfg, + transferManager: transfer.NewManager(3), + } +} + +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +func (a *App) GetConnections() []config.ConnectionConfig { + if a.cfg == nil { + return nil + } + return a.cfg.Connections +} + +func (a *App) SaveConnection(c config.ConnectionConfig, secret string) error { + if c.ID == "" { + return fmt.Errorf("connection ID cannot be empty") + } + + if secret != "" { + err := keychain.SetSecret(c.ID, secret) + if err != nil { + return err + } + } + + found := false + for i, existing := range a.cfg.Connections { + if existing.ID == c.ID { + a.cfg.Connections[i] = c + found = true + break + } + } + if !found { + a.cfg.Connections = append(a.cfg.Connections, c) + } + + return config.SaveConfig(a.cfg) +} + +func (a *App) DeleteConnection(id string) error { + newConns := []config.ConnectionConfig{} + for _, c := range a.cfg.Connections { + if c.ID != id { + newConns = append(newConns, c) + } + } + a.cfg.Connections = newConns + _ = keychain.DeleteSecret(id) + return config.SaveConfig(a.cfg) +} + +func (a *App) getExplorerForConnection(id string) (explorer.Explorer, error) { + if id == "local" { + return local.New(), nil + } + + var conn *config.ConnectionConfig + for _, c := range a.cfg.Connections { + if c.ID == id { + conn = &c + break + } + } + if conn == nil { + return nil, fmt.Errorf("connection not found") + } + + secret, _ := keychain.GetSecret(id) + + switch conn.Protocol { + case "s3": + return s3.New(conn, secret), nil + case "sftp": + return sftp.New(conn, secret), nil + case "smb": + return smb.New(conn, secret), nil + case "webdav": + return webdav.New(conn, secret), nil + case "nfs": + return nfs.New(conn, secret) + default: + return nil, fmt.Errorf("protocol %s not fully implemented", conn.Protocol) + } +} + +func (a *App) ListDir(connId, path string) ([]explorer.FileEntry, error) { + exp, err := a.getExplorerForConnection(connId) + if err != nil { + return nil, err + } + if err := exp.Connect(); err != nil { + return nil, err + } + defer exp.Disconnect() + return exp.ListDir(path) +} + +func (a *App) MkDir(connId, path string) error { + exp, err := a.getExplorerForConnection(connId) + if err != nil { + return err + } + if err := exp.Connect(); err != nil { + return err + } + defer exp.Disconnect() + return exp.MkDir(path) +} + +func (a *App) Delete(connId, path string) error { + exp, err := a.getExplorerForConnection(connId) + if err != nil { + return err + } + if err := exp.Connect(); err != nil { + return err + } + defer exp.Disconnect() + return exp.Delete(path) +} + +func (a *App) QueueTransfer(id string, srcConnId, dstConnId, srcPath, dstPath, filename string, size int64) error { + srcExp, err := a.getExplorerForConnection(srcConnId) + if err != nil { + return err + } + dstExp, err := a.getExplorerForConnection(dstConnId) + if err != nil { + return err + } + + if err := srcExp.Connect(); err != nil { + return err + } + if err := dstExp.Connect(); err != nil { + srcExp.Disconnect() + return err + } + + return a.transferManager.QueueTransfer(id, srcPath, dstPath, filename, size, srcExp, dstExp) +} + +func (a *App) GetTransfers() []*transfer.Transfer { + return a.transferManager.GetTransfers() +} + +func (a *App) Rename(connId, src, dst string) error { + exp, err := a.getExplorerForConnection(connId) + if err != nil { + return err + } + if err := exp.Connect(); err != nil { + return err + } + defer exp.Disconnect() + return exp.Rename(src, dst) +} + +func (a *App) PromptUploadFiles(connId, destPath string) error { + files, err := runtime.OpenMultipleFilesDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select Files to Upload", + }) + if err != nil { + return err + } + + for _, localPath := range files { + stat, err := os.Stat(localPath) + if err != nil { + continue + } + id := uuid.New().String() + fileName := filepath.Base(localPath) + remotePath := destPath + if destPath == "" || destPath == "/" { + remotePath = fileName + } else if destPath[len(destPath)-1] != '/' { + remotePath = destPath + "/" + fileName + } else { + remotePath = destPath + fileName + } + + a.QueueTransfer(id, "local", connId, localPath, remotePath, fileName, stat.Size()) + } + return nil +} + +func (a *App) PromptUploadDirectory(connId, destPath string) error { + dir, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select Directory to Upload", + }) + if err != nil || dir == "" { + return err + } + + baseDirName := filepath.Base(dir) + remoteBaseDir := destPath + if remoteBaseDir == "" || remoteBaseDir == "/" { + remoteBaseDir = baseDirName + } else if remoteBaseDir[len(remoteBaseDir)-1] != '/' { + remoteBaseDir = remoteBaseDir + "/" + baseDirName + } else { + remoteBaseDir = remoteBaseDir + baseDirName + } + + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, _ := filepath.Rel(dir, path) + if relPath == "." { + return a.MkDir(connId, remoteBaseDir) + } + + remotePath := remoteBaseDir + "/" + filepath.ToSlash(relPath) + + if info.IsDir() { + return a.MkDir(connId, remotePath) + } + + id := uuid.New().String() + a.QueueTransfer(id, "local", connId, path, remotePath, info.Name(), info.Size()) + return nil + }) +} + +func (a *App) PromptDownload(connId, remotePath string) error { + fileName := filepath.Base(remotePath) + localPath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "Save File", + DefaultFilename: fileName, + }) + if err != nil || localPath == "" { + return err + } + + id := uuid.New().String() + + exp, err := a.getExplorerForConnection(connId) + if err != nil { + return err + } + if err := exp.Connect(); err != nil { + return err + } + stat, err := exp.Stat(remotePath) + exp.Disconnect() + + size := int64(0) + if err == nil { + size = stat.Size + } + + return a.QueueTransfer(id, connId, "local", remotePath, localPath, fileName, size) +} diff --git a/build/README.md b/build/README.md new file mode 100644 index 0000000..1ae2f67 --- /dev/null +++ b/build/README.md @@ -0,0 +1,35 @@ +# Build Directory + +The build directory is used to house all the build files and assets for your application. + +The structure is: + +* bin - Output directory +* darwin - macOS specific files +* windows - Windows specific files + +## Mac + +The `darwin` directory holds files specific to Mac builds. +These may be customised and used as part of the build. To return these files to the default state, simply delete them +and +build with `wails build`. + +The directory contains the following files: + +- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. +- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. + +## Windows + +The `windows` directory contains the manifest and rc files used when building with `wails build`. +These may be customised for your application. To return these files to the default state, simply delete them and +build with `wails build`. + +- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to + use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file + will be created using the `appicon.png` file in the build directory. +- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. +- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, + as well as the application itself (right click the exe -> properties -> details) +- `wails.exe.manifest` - The main application manifest file. \ No newline at end of file diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/build/appicon.png differ diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist new file mode 100644 index 0000000..14121ef --- /dev/null +++ b/build/darwin/Info.dev.plist @@ -0,0 +1,68 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist new file mode 100644 index 0000000..d17a747 --- /dev/null +++ b/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{.Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000..f334798 Binary files /dev/null and b/build/windows/icon.ico differ diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/build/windows/installer/project.nsi b/build/windows/installer/project.nsi new file mode 100644 index 0000000..654ae2e --- /dev/null +++ b/build/windows/installer/project.nsi @@ -0,0 +1,114 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the ProjectInfo file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}" +## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}" +## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}" +## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + !insertmacro wails.associateCustomProtocols + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + !insertmacro wails.unassociateCustomProtocols + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/installer/wails_tools.nsh new file mode 100644 index 0000000..2f6d321 --- /dev/null +++ b/build/windows/installer/wails_tools.nsh @@ -0,0 +1,249 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "{{.Name}}" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "{{.Info.CompanyName}}" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "{{.Info.ProductName}}" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "{{.Info.Copyright}}" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "tmp\MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + {{range .Info.FileAssociations}} + !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + File "..\{{.IconName}}.ico" + {{end}} +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + {{range .Info.FileAssociations}} + !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" + + Delete "$INSTDIR\{{.IconName}}.ico" + {{end}} +!macroend + +!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" +!macroend + +!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL + DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" +!macroend + +!macro wails.associateCustomProtocols + ; Create custom protocols associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" + + {{end}} +!macroend + +!macro wails.unassociateCustomProtocols + ; Delete app custom protocol associations + {{range .Info.Protocols}} + !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" + {{end}} +!macroend diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest new file mode 100644 index 0000000..17e1a23 --- /dev/null +++ b/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..73cb12b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,143 @@ + + + + + + GoExplore + + + + + +
+
+ +
+
+ + +
+ + +
+
+
+ + + + + + + + + + + + +
NameSizeModifiedPermissions
+
+
+
+

Transfers

+ +
+
+
+
+
+ + + + + + + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..ae90095 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,664 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "devDependencies": { + "vite": "^3.0.7" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/vite": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz", + "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a1b6f8e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^3.0.7" + } +} \ No newline at end of file diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100755 index 0000000..d4671a1 --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +5fbf12469d224a93954efecb5886e8a6 \ No newline at end of file diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..59d06f6 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,54 @@ +#logo { + display: block; + width: 50%; + height: 50%; + margin: auto; + padding: 10% 0 0; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + background-origin: content-box; +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; +} + +.input-box .btn { + width: 60px; + height: 30px; + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/frontend/src/assets/appicon.png b/frontend/src/assets/appicon.png new file mode 100644 index 0000000..63617fe Binary files /dev/null and b/frontend/src/assets/appicon.png differ diff --git a/frontend/src/assets/fonts/OFL.txt b/frontend/src/assets/fonts/OFL.txt new file mode 100644 index 0000000..9cac04c --- /dev/null +++ b/frontend/src/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com), + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 new file mode 100644 index 0000000..2f9cc59 Binary files /dev/null and b/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2 differ diff --git a/frontend/src/assets/images/logo-universal.png b/frontend/src/assets/images/logo-universal.png new file mode 100644 index 0000000..d63303b Binary files /dev/null and b/frontend/src/assets/images/logo-universal.png differ diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..40bd829 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,273 @@ +import { ListDir, GetConnections, SaveConnection, DeleteConnection, Delete, Rename, PromptUploadFiles, PromptUploadDirectory, PromptDownload } from '../wailsjs/go/main/App.js'; + +let currentConn = 'local'; +let currentPath = ''; +let connectionsCache = []; + +function uuidv4() { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); +} + +async function init() { + await loadConnections(); + await loadDirectory(currentConn, currentPath); + + document.getElementById('conn-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const conn = { + id: document.getElementById('conn-id').value, + name: document.getElementById('conn-name').value, + protocol: document.getElementById('conn-protocol').value, + host: document.getElementById('conn-host').value, + port: parseInt(document.getElementById('conn-port').value) || 0, + bucket: document.getElementById('conn-bucket').value, + region: document.getElementById('conn-region').value, + path_style: document.getElementById('conn-pathstyle').checked, + username: document.getElementById('conn-username').value + }; + const secret = document.getElementById('conn-secret').value; + + try { + await SaveConnection(conn, secret); + closeConnModal(); + loadConnections(); + } catch(err) { + alert("Failed to save connection: " + err); + } + }); +} + +async function loadConnections() { + const list = document.getElementById('conn-list'); + list.innerHTML = `
+ OS Local Filesystem +
`; + + try { + const conns = await GetConnections(); + connectionsCache = conns || []; + if (connectionsCache) { + connectionsCache.forEach(c => { + const el = document.createElement('div'); + el.className = `conn-item ${currentConn === c.id ? 'active' : ''}`; + el.innerHTML = `${c.protocol} ${c.name} `; + el.onclick = () => switchConn(c.id); + list.appendChild(el); + }); + } + } catch(e) { + console.error(e); + } +} + +window.switchConn = async (id) => { + currentConn = id; + currentPath = ''; // Root path + loadConnections(); // To update 'active' class + await loadDirectory(currentConn, currentPath); +}; + +window.editConnection = (id) => { + const c = connectionsCache.find(conn => conn.id === id); + if (!c) return; + document.getElementById('modal-title').innerText = "Edit Connection"; + document.getElementById('conn-id').value = c.id; + document.getElementById('conn-name').value = c.name; + document.getElementById('conn-protocol').value = c.protocol; + document.getElementById('conn-host').value = c.host || ''; + document.getElementById('conn-port').value = c.port || ''; + document.getElementById('conn-bucket').value = c.bucket || ''; + document.getElementById('conn-region').value = c.region || ''; + document.getElementById('conn-pathstyle').checked = c.path_style || false; + document.getElementById('conn-username').value = c.username || ''; + document.getElementById('conn-secret').value = ''; + document.getElementById('conn-delete-btn').style.display = 'block'; + + updateProtocolFields(); + document.getElementById('conn-modal').style.display = 'flex'; +}; + +window.openConnModal = () => { + document.getElementById('modal-title').innerText = "Add Connection"; + document.getElementById('conn-form').reset(); + document.getElementById('conn-id').value = uuidv4(); + document.getElementById('conn-delete-btn').style.display = 'none'; + updateProtocolFields(); + document.getElementById('conn-modal').style.display = 'flex'; +}; + +window.closeConnModal = () => { + document.getElementById('conn-modal').style.display = 'none'; +}; + +window.updateProtocolFields = () => { + const protocol = document.getElementById('conn-protocol').value; + + const showBucket = protocol === 's3' || protocol === 'smb'; + document.getElementById('bucket-fields').style.display = showBucket ? 'block' : 'none'; + + // Only show S3 specific fields when S3 is selected + const showS3Specific = protocol === 's3'; + document.getElementById('region-group').style.display = showS3Specific ? 'flex' : 'none'; + document.getElementById('pathstyle-group').style.display = showS3Specific ? 'flex' : 'none'; +}; + +window.deleteConnection = async () => { + if(!confirm("Are you sure you want to delete this connection?")) return; + const id = document.getElementById('conn-id').value; + try { + await DeleteConnection(id); + closeConnModal(); + if(currentConn === id) { + switchConn('local'); + } else { + loadConnections(); + } + } catch(err) { + alert("Failed to delete connection: " + err); + } +}; + +async function loadDirectory(connId, path) { + const tbody = document.getElementById('file-list'); + tbody.innerHTML = 'Loading...'; + document.getElementById('breadcrumb').innerText = path || '/'; + + try { + const files = await ListDir(connId, path); + tbody.innerHTML = ''; + if (!files || files.length === 0) { + tbody.innerHTML = 'Empty directory'; + return; + } + + files.forEach(f => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${f.is_dir ? '📁 ' : '📄 '}${f.name} + ${f.is_dir ? '-' : formatBytes(f.size)} + ${f.modified} + ${f.permissions} + `; + if (f.is_dir) { + tr.ondblclick = () => { + currentPath = f.path; + loadDirectory(currentConn, currentPath); + }; + } + tr.dataset.path = f.path; + tr.dataset.name = f.name; + tbody.appendChild(tr); + }); + } catch(e) { + tbody.innerHTML = `Error: ${e}`; + } +} + +window.refreshCurrentDir = () => { + loadDirectory(currentConn, currentPath); +}; + +window.showTransfers = () => { + document.getElementById('transfer-tray').style.display = 'block'; +}; +window.hideTransfers = () => { + document.getElementById('transfer-tray').style.display = 'none'; +}; + +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +window.addEventListener('load', init); + +// Context Menu Logic +let contextMenuTarget = null; + +document.addEventListener('contextmenu', (e) => { + const menu = document.getElementById('context-menu'); + menu.style.display = 'none'; + + const row = e.target.closest('tr'); + if (row && row.dataset.path) { + e.preventDefault(); + contextMenuTarget = { + path: row.dataset.path, + name: row.dataset.name + }; + menu.style.display = 'block'; + menu.style.left = `${e.pageX}px`; + menu.style.top = `${e.pageY}px`; + } +}); + +document.addEventListener('click', () => { + document.getElementById('context-menu').style.display = 'none'; + const uploadMenu = document.getElementById('upload-menu'); + if (uploadMenu) uploadMenu.style.display = 'none'; +}); + +window.toggleUploadMenu = (e) => { + if (e) e.stopPropagation(); + const menu = document.getElementById('upload-menu'); + if (menu.style.display === 'none') { + menu.style.display = 'block'; + } else { + menu.style.display = 'none'; + } +}; + +window.handleContextMenu = async (action) => { + if (!contextMenuTarget) return; + + const { path, name } = contextMenuTarget; + + try { + if (action === 'delete') { + if (confirm(`Are you sure you want to delete ${name}?`)) { + await Delete(currentConn, path); + refreshCurrentDir(); + } + } else if (action === 'rename') { + const newName = prompt(`Enter new name for ${name}:`, name); + if (newName && newName !== name) { + // Construct new path by replacing the last part + const pathParts = path.split('/'); + pathParts[pathParts.length - 1] = newName; + const newPath = pathParts.join('/'); + + await Rename(currentConn, path, newPath); + refreshCurrentDir(); + } + } else if (action === 'download') { + await PromptDownload(currentConn, path); + showTransfers(); + } + } catch (e) { + alert(`Failed to ${action}: ${e}`); + } +}; + +window.promptUploadFiles = async () => { + try { + await PromptUploadFiles(currentConn, currentPath); + showTransfers(); + } catch (e) { + alert("Upload failed: " + e); + } +}; + +window.promptUploadDirectory = async () => { + try { + await PromptUploadDirectory(currentConn, currentPath); + showTransfers(); + } catch (e) { + alert("Upload failed: " + e); + } +}; diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..55cd12b --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,303 @@ +:root { + color-scheme: dark; + --bg: #0a0a0a; + --surface: #141414; + --surface-hover: #1f1f1f; + --border: #2a2a2a; + --text-primary: #f0f0f0; + --text-secondary: #888888; + --accent: #21D198; + --accent-hover: #1cb582; + --success: #21D198; + --warning: #f59e0b; + --error: #ef4444; + --font: 'Inter', system-ui, -apple-system, sans-serif; +} + +body { + margin: 0; + padding: 0; + background-color: var(--bg); + color: var(--text-primary); + font-family: var(--font); + display: flex; + height: 100vh; + overflow: hidden; +} + +#sidebar { + width: 250px; + background: var(--surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; +} + +.sidebar-header { + padding: 1rem; + font-size: 1.25rem; + font-weight: bold; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + color: var(--accent); +} + +.conn-list { + flex: 1; + overflow-y: auto; + padding: 1rem 0; +} + +.conn-item { + padding: 0.75rem 1rem; + cursor: pointer; + display: flex; + align-items: center; + transition: background 0.2s; +} + +.conn-item:hover { + background: var(--surface-hover); +} + +.conn-item.active { + border-left: 3px solid var(--accent); + background: var(--surface-hover); +} + +.badge { + padding: 0.15rem 0.4rem; + border-radius: 4px; + font-size: 0.6rem; + font-weight: 600; + text-transform: uppercase; + margin-right: 0.5rem; + background: rgba(33, 209, 152, 0.1); + color: var(--accent); + border: 1px solid rgba(33, 209, 152, 0.3); +} + +#main-area { + flex: 1; + display: flex; + flex-direction: column; + position: relative; +} + +#toolbar { + height: 60px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + padding: 0 1rem; + background: var(--surface); +} + +#breadcrumb { + flex: 1; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.crumb { + cursor: pointer; +} +.crumb:hover { + color: var(--text-primary); + text-decoration: underline; +} + +#browser-pane { + flex: 1; + overflow-y: auto; + padding: 1rem; +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +th, td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border); +} + +th { + color: var(--text-secondary); + font-weight: 500; + text-transform: uppercase; + font-size: 0.75rem; +} + +tbody tr { + transition: background 0.2s; + cursor: pointer; +} + +tbody tr:hover { + background: var(--surface-hover); +} + +.btn { + background: var(--accent); + color: #000; + border: 1px solid var(--accent); + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + font-size: 0.875rem; + transition: all 0.2s ease; +} + +.btn:hover { + background: var(--accent-hover); + box-shadow: 0 0 10px rgba(33, 209, 152, 0.3); +} + +#transfer-tray { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: var(--surface); + border-top: 1px solid var(--border); + max-height: 200px; + overflow-y: auto; + padding: 1rem; + display: none; +} + +.transfer-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); +} + +.progress-bar { + flex: 1; + height: 8px; + background: #000; + border-radius: 4px; + margin: 0 1rem; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: var(--accent); + width: 0%; +} + +.transfer-meta { + font-size: 0.75rem; + color: var(--text-secondary); + width: 200px; + text-align: right; +} + +.modal-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: none; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 2rem; + width: 500px; + max-width: 90vw; + box-shadow: 0 10px 25px rgba(0,0,0,0.5); +} + +.form-group { + margin-bottom: 1rem; + display: flex; + flex-direction: column; +} + +.form-row { + display: flex; + gap: 1rem; +} + +label { + font-size: 0.8rem; + color: var(--text-secondary); + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +input[type="text"], input[type="password"], input[type="number"], select { + background: #0a0a0a; + border: 1px solid var(--border); + color: var(--text-primary); + padding: 0.5rem; + border-radius: 4px; + font-family: inherit; + font-size: 0.9rem; + width: 100%; + box-sizing: border-box; +} + +input:focus, select:focus { + outline: none; + border-color: var(--accent); +} + +select option { + background-color: var(--surface); + color: var(--text-primary); +} + +.btn-danger { + background: transparent; + color: var(--error); + border: 1px solid var(--error); +} + +.btn-danger:hover { + background: rgba(239, 68, 68, 0.1); + box-shadow: none; +} + +/* Context Menu */ +.context-menu { + position: absolute; + background: var(--surface); + border: 1px solid var(--border); + box-shadow: 0 4px 6px rgba(0,0,0,0.3); + border-radius: 4px; + padding: 0.5rem 0; + z-index: 1000; + min-width: 150px; +} + +.menu-item { + padding: 0.5rem 1rem; + cursor: pointer; + font-size: 0.9rem; +} + +.menu-item:hover { + background: var(--surface-hover); +} + +.action-bar { + display: flex; + gap: 0.5rem; +} diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100755 index 0000000..6b90e6f --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -0,0 +1,29 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {config} from '../models'; +import {transfer} from '../models'; +import {explorer} from '../models'; + +export function Delete(arg1:string,arg2:string):Promise; + +export function DeleteConnection(arg1:string):Promise; + +export function GetConnections():Promise>; + +export function GetTransfers():Promise>; + +export function ListDir(arg1:string,arg2:string):Promise>; + +export function MkDir(arg1:string,arg2:string):Promise; + +export function PromptDownload(arg1:string,arg2:string):Promise; + +export function PromptUploadDirectory(arg1:string,arg2:string):Promise; + +export function PromptUploadFiles(arg1:string,arg2:string):Promise; + +export function QueueTransfer(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string,arg6:string,arg7:number):Promise; + +export function Rename(arg1:string,arg2:string,arg3:string):Promise; + +export function SaveConnection(arg1:config.ConnectionConfig,arg2:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100755 index 0000000..64acde2 --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -0,0 +1,51 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export function Delete(arg1, arg2) { + return window['go']['main']['App']['Delete'](arg1, arg2); +} + +export function DeleteConnection(arg1) { + return window['go']['main']['App']['DeleteConnection'](arg1); +} + +export function GetConnections() { + return window['go']['main']['App']['GetConnections'](); +} + +export function GetTransfers() { + return window['go']['main']['App']['GetTransfers'](); +} + +export function ListDir(arg1, arg2) { + return window['go']['main']['App']['ListDir'](arg1, arg2); +} + +export function MkDir(arg1, arg2) { + return window['go']['main']['App']['MkDir'](arg1, arg2); +} + +export function PromptDownload(arg1, arg2) { + return window['go']['main']['App']['PromptDownload'](arg1, arg2); +} + +export function PromptUploadDirectory(arg1, arg2) { + return window['go']['main']['App']['PromptUploadDirectory'](arg1, arg2); +} + +export function PromptUploadFiles(arg1, arg2) { + return window['go']['main']['App']['PromptUploadFiles'](arg1, arg2); +} + +export function QueueTransfer(arg1, arg2, arg3, arg4, arg5, arg6, arg7) { + return window['go']['main']['App']['QueueTransfer'](arg1, arg2, arg3, arg4, arg5, arg6, arg7); +} + +export function Rename(arg1, arg2, arg3) { + return window['go']['main']['App']['Rename'](arg1, arg2, arg3); +} + +export function SaveConnection(arg1, arg2) { + return window['go']['main']['App']['SaveConnection'](arg1, arg2); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts new file mode 100755 index 0000000..cadcfc3 --- /dev/null +++ b/frontend/wailsjs/go/models.ts @@ -0,0 +1,97 @@ +export namespace config { + + export class ConnectionConfig { + id: string; + name: string; + protocol: string; + host?: string; + port?: number; + bucket?: string; + region?: string; + path_style?: boolean; + username?: string; + keychain_key?: string; + + static createFrom(source: any = {}) { + return new ConnectionConfig(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.name = source["name"]; + this.protocol = source["protocol"]; + this.host = source["host"]; + this.port = source["port"]; + this.bucket = source["bucket"]; + this.region = source["region"]; + this.path_style = source["path_style"]; + this.username = source["username"]; + this.keychain_key = source["keychain_key"]; + } + } + +} + +export namespace explorer { + + export class FileEntry { + name: string; + path: string; + size: number; + modified: string; + is_dir: boolean; + permissions: string; + + static createFrom(source: any = {}) { + return new FileEntry(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.path = source["path"]; + this.size = source["size"]; + this.modified = source["modified"]; + this.is_dir = source["is_dir"]; + this.permissions = source["permissions"]; + } + } + +} + +export namespace transfer { + + export class Transfer { + id: string; + source: string; + destination: string; + filename: string; + bytes_total: number; + bytes_done: number; + speed_mbps: number; + eta_seconds: number; + status: string; + error?: string; + + static createFrom(source: any = {}) { + return new Transfer(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.source = source["source"]; + this.destination = source["destination"]; + this.filename = source["filename"]; + this.bytes_total = source["bytes_total"]; + this.bytes_done = source["bytes_done"]; + this.speed_mbps = source["speed_mbps"]; + this.eta_seconds = source["eta_seconds"]; + this.status = source["status"]; + this.error = source["error"]; + } + } + +} + diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/wailsjs/runtime/package.json @@ -0,0 +1,24 @@ +{ + "name": "@wailsapp/runtime", + "version": "2.0.0", + "description": "Wails Javascript runtime library", + "main": "runtime.js", + "types": "runtime.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wailsapp/wails.git" + }, + "keywords": [ + "Wails", + "Javascript", + "Go" + ], + "author": "Lea Anthony ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..3bbea84 --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -0,0 +1,330 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Position { + x: number; + y: number; +} + +export interface Size { + w: number; + h: number; +} + +export interface Screen { + isCurrent: boolean; + isPrimary: boolean; + width : number + height : number +} + +// Environment information such as platform, buildtype, ... +export interface EnvironmentInfo { + buildType: string; + platform: string; + arch: string; +} + +// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) +// emits the given event. Optional data may be passed with the event. +// This will trigger any event listeners. +export function EventsEmit(eventName: string, ...data: any): void; + +// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. +export function EventsOn(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) +// sets up a listener for the given event name, but will only trigger a given number times. +export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void; + +// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) +// sets up a listener for the given event name, but will only trigger once. +export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void; + +// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) +// unregisters the listener for the given event name. +export function EventsOff(eventName: string, ...additionalEventNames: string[]): void; + +// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) +// unregisters all listeners. +export function EventsOffAll(): void; + +// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) +// logs the given message as a raw message +export function LogPrint(message: string): void; + +// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) +// logs the given message at the `trace` log level. +export function LogTrace(message: string): void; + +// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) +// logs the given message at the `debug` log level. +export function LogDebug(message: string): void; + +// [LogError](https://wails.io/docs/reference/runtime/log#logerror) +// logs the given message at the `error` log level. +export function LogError(message: string): void; + +// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) +// logs the given message at the `fatal` log level. +// The application will quit after calling this method. +export function LogFatal(message: string): void; + +// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) +// logs the given message at the `info` log level. +export function LogInfo(message: string): void; + +// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) +// logs the given message at the `warning` log level. +export function LogWarning(message: string): void; + +// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) +// Forces a reload by the main application as well as connected browsers. +export function WindowReload(): void; + +// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) +// Reloads the application frontend. +export function WindowReloadApp(): void; + +// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) +// Sets the window AlwaysOnTop or not on top. +export function WindowSetAlwaysOnTop(b: boolean): void; + +// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) +// *Windows only* +// Sets window theme to system default (dark/light). +export function WindowSetSystemDefaultTheme(): void; + +// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) +// *Windows only* +// Sets window to light theme. +export function WindowSetLightTheme(): void; + +// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) +// *Windows only* +// Sets window to dark theme. +export function WindowSetDarkTheme(): void; + +// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) +// Centers the window on the monitor the window is currently on. +export function WindowCenter(): void; + +// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) +// Sets the text in the window title bar. +export function WindowSetTitle(title: string): void; + +// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) +// Makes the window full screen. +export function WindowFullscreen(): void; + +// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) +// Restores the previous window dimensions and position prior to full screen. +export function WindowUnfullscreen(): void; + +// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) +// Returns the state of the window, i.e. whether the window is in full screen mode or not. +export function WindowIsFullscreen(): Promise; + +// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) +// Sets the width and height of the window. +export function WindowSetSize(width: number, height: number): void; + +// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) +// Gets the width and height of the window. +export function WindowGetSize(): Promise; + +// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) +// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMaxSize(width: number, height: number): void; + +// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) +// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. +// Setting a size of 0,0 will disable this constraint. +export function WindowSetMinSize(width: number, height: number): void; + +// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) +// Sets the window position relative to the monitor the window is currently on. +export function WindowSetPosition(x: number, y: number): void; + +// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) +// Gets the window position relative to the monitor the window is currently on. +export function WindowGetPosition(): Promise; + +// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) +// Hides the window. +export function WindowHide(): void; + +// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) +// Shows the window, if it is currently hidden. +export function WindowShow(): void; + +// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) +// Maximises the window to fill the screen. +export function WindowMaximise(): void; + +// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) +// Toggles between Maximised and UnMaximised. +export function WindowToggleMaximise(): void; + +// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) +// Restores the window to the dimensions and position prior to maximising. +export function WindowUnmaximise(): void; + +// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) +// Returns the state of the window, i.e. whether the window is maximised or not. +export function WindowIsMaximised(): Promise; + +// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) +// Minimises the window. +export function WindowMinimise(): void; + +// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) +// Restores the window to the dimensions and position prior to minimising. +export function WindowUnminimise(): void; + +// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) +// Returns the state of the window, i.e. whether the window is minimised or not. +export function WindowIsMinimised(): Promise; + +// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) +// Returns the state of the window, i.e. whether the window is normal or not. +export function WindowIsNormal(): Promise; + +// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) +// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. +export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void; + +// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) +// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. +export function ScreenGetAll(): Promise; + +// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) +// Opens the given URL in the system browser. +export function BrowserOpenURL(url: string): void; + +// [Environment](https://wails.io/docs/reference/runtime/intro#environment) +// Returns information about the environment +export function Environment(): Promise; + +// [Quit](https://wails.io/docs/reference/runtime/intro#quit) +// Quits the application. +export function Quit(): void; + +// [Hide](https://wails.io/docs/reference/runtime/intro#hide) +// Hides the application. +export function Hide(): void; + +// [Show](https://wails.io/docs/reference/runtime/intro#show) +// Shows the application. +export function Show(): void; + +// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) +// Returns the current text stored on clipboard +export function ClipboardGetText(): Promise; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop) +// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. +export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void + +// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff) +// OnFileDropOff removes the drag and drop listeners and handlers. +export function OnFileDropOff() :void + +// Check if the file path resolver is available +export function CanResolveFilePaths(): boolean; + +// Resolves file paths for an array of files +export function ResolveFilePaths(files: File[]): void + +// Notification types +export interface NotificationOptions { + id: string; + title: string; + subtitle?: string; // macOS and Linux only + body?: string; + categoryId?: string; + data?: { [key: string]: any }; +} + +export interface NotificationAction { + id?: string; + title?: string; + destructive?: boolean; // macOS-specific +} + +export interface NotificationCategory { + id?: string; + actions?: NotificationAction[]; + hasReplyField?: boolean; + replyPlaceholder?: string; + replyButtonTitle?: string; +} + +// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications) +// Initializes the notification service for the application. +// This must be called before sending any notifications. +export function InitializeNotifications(): Promise; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions) +// Sends a notification with action buttons. Requires a registered category. +export function SendNotificationWithActions(options: NotificationOptions): Promise; + +// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory) +// Registers a notification category that can be used with SendNotificationWithActions. +export function RegisterNotificationCategory(category: NotificationCategory): Promise; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification) +// Removes a notification by its identifier (cross-platform convenience function). +export function RemoveNotification(identifier: string): Promise; \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..556621e --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.js @@ -0,0 +1,298 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export function LogPrint(message) { + window.runtime.LogPrint(message); +} + +export function LogTrace(message) { + window.runtime.LogTrace(message); +} + +export function LogDebug(message) { + window.runtime.LogDebug(message); +} + +export function LogInfo(message) { + window.runtime.LogInfo(message); +} + +export function LogWarning(message) { + window.runtime.LogWarning(message); +} + +export function LogError(message) { + window.runtime.LogError(message); +} + +export function LogFatal(message) { + window.runtime.LogFatal(message); +} + +export function EventsOnMultiple(eventName, callback, maxCallbacks) { + return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); +} + +export function EventsOn(eventName, callback) { + return EventsOnMultiple(eventName, callback, -1); +} + +export function EventsOff(eventName, ...additionalEventNames) { + return window.runtime.EventsOff(eventName, ...additionalEventNames); +} + +export function EventsOffAll() { + return window.runtime.EventsOffAll(); +} + +export function EventsOnce(eventName, callback) { + return EventsOnMultiple(eventName, callback, 1); +} + +export function EventsEmit(eventName) { + let args = [eventName].slice.call(arguments); + return window.runtime.EventsEmit.apply(null, args); +} + +export function WindowReload() { + window.runtime.WindowReload(); +} + +export function WindowReloadApp() { + window.runtime.WindowReloadApp(); +} + +export function WindowSetAlwaysOnTop(b) { + window.runtime.WindowSetAlwaysOnTop(b); +} + +export function WindowSetSystemDefaultTheme() { + window.runtime.WindowSetSystemDefaultTheme(); +} + +export function WindowSetLightTheme() { + window.runtime.WindowSetLightTheme(); +} + +export function WindowSetDarkTheme() { + window.runtime.WindowSetDarkTheme(); +} + +export function WindowCenter() { + window.runtime.WindowCenter(); +} + +export function WindowSetTitle(title) { + window.runtime.WindowSetTitle(title); +} + +export function WindowFullscreen() { + window.runtime.WindowFullscreen(); +} + +export function WindowUnfullscreen() { + window.runtime.WindowUnfullscreen(); +} + +export function WindowIsFullscreen() { + return window.runtime.WindowIsFullscreen(); +} + +export function WindowGetSize() { + return window.runtime.WindowGetSize(); +} + +export function WindowSetSize(width, height) { + window.runtime.WindowSetSize(width, height); +} + +export function WindowSetMaxSize(width, height) { + window.runtime.WindowSetMaxSize(width, height); +} + +export function WindowSetMinSize(width, height) { + window.runtime.WindowSetMinSize(width, height); +} + +export function WindowSetPosition(x, y) { + window.runtime.WindowSetPosition(x, y); +} + +export function WindowGetPosition() { + return window.runtime.WindowGetPosition(); +} + +export function WindowHide() { + window.runtime.WindowHide(); +} + +export function WindowShow() { + window.runtime.WindowShow(); +} + +export function WindowMaximise() { + window.runtime.WindowMaximise(); +} + +export function WindowToggleMaximise() { + window.runtime.WindowToggleMaximise(); +} + +export function WindowUnmaximise() { + window.runtime.WindowUnmaximise(); +} + +export function WindowIsMaximised() { + return window.runtime.WindowIsMaximised(); +} + +export function WindowMinimise() { + window.runtime.WindowMinimise(); +} + +export function WindowUnminimise() { + window.runtime.WindowUnminimise(); +} + +export function WindowSetBackgroundColour(R, G, B, A) { + window.runtime.WindowSetBackgroundColour(R, G, B, A); +} + +export function ScreenGetAll() { + return window.runtime.ScreenGetAll(); +} + +export function WindowIsMinimised() { + return window.runtime.WindowIsMinimised(); +} + +export function WindowIsNormal() { + return window.runtime.WindowIsNormal(); +} + +export function BrowserOpenURL(url) { + window.runtime.BrowserOpenURL(url); +} + +export function Environment() { + return window.runtime.Environment(); +} + +export function Quit() { + window.runtime.Quit(); +} + +export function Hide() { + window.runtime.Hide(); +} + +export function Show() { + window.runtime.Show(); +} + +export function ClipboardGetText() { + return window.runtime.ClipboardGetText(); +} + +export function ClipboardSetText(text) { + return window.runtime.ClipboardSetText(text); +} + +/** + * Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * + * @export + * @callback OnFileDropCallback + * @param {number} x - x coordinate of the drop + * @param {number} y - y coordinate of the drop + * @param {string[]} paths - A list of file paths. + */ + +/** + * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings. + * + * @export + * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished. + * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target) + */ +export function OnFileDrop(callback, useDropTarget) { + return window.runtime.OnFileDrop(callback, useDropTarget); +} + +/** + * OnFileDropOff removes the drag and drop listeners and handlers. + */ +export function OnFileDropOff() { + return window.runtime.OnFileDropOff(); +} + +export function CanResolveFilePaths() { + return window.runtime.CanResolveFilePaths(); +} + +export function ResolveFilePaths(files) { + return window.runtime.ResolveFilePaths(files); +} + +export function InitializeNotifications() { + return window.runtime.InitializeNotifications(); +} + +export function CleanupNotifications() { + return window.runtime.CleanupNotifications(); +} + +export function IsNotificationAvailable() { + return window.runtime.IsNotificationAvailable(); +} + +export function RequestNotificationAuthorization() { + return window.runtime.RequestNotificationAuthorization(); +} + +export function CheckNotificationAuthorization() { + return window.runtime.CheckNotificationAuthorization(); +} + +export function SendNotification(options) { + return window.runtime.SendNotification(options); +} + +export function SendNotificationWithActions(options) { + return window.runtime.SendNotificationWithActions(options); +} + +export function RegisterNotificationCategory(category) { + return window.runtime.RegisterNotificationCategory(category); +} + +export function RemoveNotificationCategory(categoryId) { + return window.runtime.RemoveNotificationCategory(categoryId); +} + +export function RemoveAllPendingNotifications() { + return window.runtime.RemoveAllPendingNotifications(); +} + +export function RemovePendingNotification(identifier) { + return window.runtime.RemovePendingNotification(identifier); +} + +export function RemoveAllDeliveredNotifications() { + return window.runtime.RemoveAllDeliveredNotifications(); +} + +export function RemoveDeliveredNotification(identifier) { + return window.runtime.RemoveDeliveredNotification(identifier); +} + +export function RemoveNotification(identifier) { + return window.runtime.RemoveNotification(identifier); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6f8b7e5 --- /dev/null +++ b/go.mod @@ -0,0 +1,68 @@ +module goexplore + +go 1.26 + +require ( + github.com/aws/aws-sdk-go-v2 v1.42.0 + github.com/aws/aws-sdk-go-v2/config v1.32.24 + github.com/aws/aws-sdk-go-v2/credentials v1.19.23 + github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 + github.com/google/uuid v1.6.0 + github.com/hirochachacha/go-smb2 v1.1.0 + github.com/pkg/sftp v1.13.10 + github.com/studio-b12/gowebdav v0.12.0 + github.com/vmware/go-nfs-client v0.0.0-20190605212624-d43b92724c1b + github.com/wailsapp/wails/v2 v2.12.0 + github.com/zalando/go-keyring v0.2.8 + golang.org/x/crypto v0.53.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/smithy-go v1.27.1 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/danieljoos/wincred v1.2.3 // indirect + github.com/geoffgarside/ber v1.1.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect +) + +// replace github.com/wailsapp/wails/v2 v2.12.0 => /home/jamie/go/pkg/mod diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3017996 --- /dev/null +++ b/go.sum @@ -0,0 +1,154 @@ +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= +git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= +github.com/aws/aws-sdk-go-v2/config v1.32.24 h1:aEDEj533yGdVvEHfkCY0D/1FbDrjnZr4pIulxRjqpHs= +github.com/aws/aws-sdk-go-v2/config v1.32.24/go.mod h1:yZtrGKJGlqfEW+/m2uTsJK+Jz7xF5R0eZfgcIG9m1ss= +github.com/aws/aws-sdk-go-v2/credentials v1.19.23 h1:Zhu3GOpRCkNjtE/gJpuPDsytSnaCCTQk8neAGsgzG5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.19.23/go.mod h1:VsJF2ropPB37gDr7M2rLSpCE8IQWdpl62uae7qxZmqU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 h1:6Xt6Ztjkwdia/7EtEaG7ki/qZUYlCcd7tGUotQed1QE= +github.com/aws/aws-sdk-go-v2/service/signin v1.1.5/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= +github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w= +github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI= +github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg= +github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM= +github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vmware/go-nfs-client v0.0.0-20190605212624-d43b92724c1b h1:RUrsc0B9xF8iC8WXrva+ULeOwN/X+zqe0FdWcDxPt/M= +github.com/vmware/go-nfs-client v0.0.0-20190605212624-d43b92724c1b/go.mod h1:psQdhrCc+fimC/8/U+PboPiIMcdmKgRdAtcMnhXhjzI= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c= +github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg= +github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= +github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..e67a603 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,71 @@ +package config + +import ( + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +type ConnectionConfig struct { + ID string `yaml:"id" json:"id"` + Name string `yaml:"name" json:"name"` + Protocol string `yaml:"protocol" json:"protocol"` + Host string `yaml:"host,omitempty" json:"host,omitempty"` + Port int `yaml:"port,omitempty" json:"port,omitempty"` + Bucket string `yaml:"bucket,omitempty" json:"bucket,omitempty"` + Region string `yaml:"region,omitempty" json:"region,omitempty"` + PathStyle bool `yaml:"path_style,omitempty" json:"path_style,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + KeychainKey string `yaml:"keychain_key,omitempty" json:"keychain_key,omitempty"` +} + +type Config struct { + Connections []ConnectionConfig `yaml:"connections"` +} + +func getConfigPath() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + dir := filepath.Join(home, ".config", "goexplore") + if err := os.MkdirAll(dir, 0755); err != nil { + return "", err + } + return filepath.Join(dir, "config.yaml"), nil +} + +func LoadConfig() (*Config, error) { + path, err := getConfigPath() + if err != nil { + return nil, err + } + + data, err := os.ReadFile(path) + if os.IsNotExist(err) { + return &Config{Connections: []ConnectionConfig{}}, nil + } else if err != nil { + return nil, err + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func SaveConfig(cfg *Config) error { + path, err := getConfigPath() + if err != nil { + return err + } + + data, err := yaml.Marshal(cfg) + if err != nil { + return err + } + + return os.WriteFile(path, data, 0644) +} diff --git a/internal/explorer/explorer.go b/internal/explorer/explorer.go new file mode 100644 index 0000000..0224ed3 --- /dev/null +++ b/internal/explorer/explorer.go @@ -0,0 +1,24 @@ +package explorer + +import "io" + +type FileEntry struct { + Name string `json:"name"` + Path string `json:"path"` + Size int64 `json:"size"` + Modified string `json:"modified"` + IsDir bool `json:"is_dir"` + Permissions string `json:"permissions"` +} + +type Explorer interface { + Connect() error + Disconnect() error + ListDir(path string) ([]FileEntry, error) + Stat(path string) (FileEntry, error) + MkDir(path string) error + Delete(path string) error + Rename(src, dst string) error + ReadFile(path string) (io.ReadCloser, error) + WriteFile(path string, r io.Reader, size int64) error +} diff --git a/internal/keychain/keychain.go b/internal/keychain/keychain.go new file mode 100644 index 0000000..155d183 --- /dev/null +++ b/internal/keychain/keychain.go @@ -0,0 +1,20 @@ +package keychain + +import "github.com/zalando/go-keyring" + +const serviceName = "goexplore" + +func SetSecret(id string, secret string) error { + key := "goexplore-" + id + return keyring.Set(serviceName, key, secret) +} + +func GetSecret(id string) (string, error) { + key := "goexplore-" + id + return keyring.Get(serviceName, key) +} + +func DeleteSecret(id string) error { + key := "goexplore-" + id + return keyring.Delete(serviceName, key) +} diff --git a/internal/protocols/local/local.go b/internal/protocols/local/local.go new file mode 100644 index 0000000..b0c73ab --- /dev/null +++ b/internal/protocols/local/local.go @@ -0,0 +1,97 @@ +package local + +import ( + "io" + "os" + "path/filepath" + "runtime" + "time" + + "goexplore/internal/explorer" +) + +type LocalExplorer struct { + basePath string +} + +func New() *LocalExplorer { + basePath := "/" + if runtime.GOOS == "windows" { + basePath = "C:\\" + } + return &LocalExplorer{basePath: basePath} +} + +func (e *LocalExplorer) Connect() error { return nil } +func (e *LocalExplorer) Disconnect() error { return nil } + +func (e *LocalExplorer) ListDir(path string) ([]explorer.FileEntry, error) { + if path == "" { + path = e.basePath + } + entries, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + var result []explorer.FileEntry + for _, entry := range entries { + info, err := entry.Info() + if err != nil { + continue + } + result = append(result, explorer.FileEntry{ + Name: entry.Name(), + Path: filepath.ToSlash(filepath.Join(path, entry.Name())), + Size: info.Size(), + Modified: info.ModTime().Format(time.RFC3339), + IsDir: entry.IsDir(), + Permissions: info.Mode().String(), + }) + } + return result, nil +} + +func (e *LocalExplorer) Stat(path string) (explorer.FileEntry, error) { + if path == "" { + path = e.basePath + } + info, err := os.Stat(path) + if err != nil { + return explorer.FileEntry{}, err + } + return explorer.FileEntry{ + Name: info.Name(), + Path: filepath.ToSlash(path), + Size: info.Size(), + Modified: info.ModTime().Format(time.RFC3339), + IsDir: info.IsDir(), + Permissions: info.Mode().String(), + }, nil +} + +func (e *LocalExplorer) MkDir(path string) error { + return os.Mkdir(path, 0755) +} + +func (e *LocalExplorer) Delete(path string) error { + return os.RemoveAll(path) +} + +func (e *LocalExplorer) Rename(src, dst string) error { + return os.Rename(src, dst) +} + +func (e *LocalExplorer) ReadFile(path string) (io.ReadCloser, error) { + return os.Open(path) +} + +func (e *LocalExplorer) WriteFile(path string, r io.Reader, size int64) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, r) + return err +} diff --git a/internal/protocols/nfs/nfs.go b/internal/protocols/nfs/nfs.go new file mode 100644 index 0000000..6e926f2 --- /dev/null +++ b/internal/protocols/nfs/nfs.go @@ -0,0 +1,125 @@ +package nfs + +import ( + "fmt" + "io" + "path/filepath" + "runtime" + "time" + + "github.com/vmware/go-nfs-client/nfs" + "github.com/vmware/go-nfs-client/nfs/rpc" + "goexplore/internal/config" + "goexplore/internal/explorer" +) + +type NFSExplorer struct { + cfg *config.ConnectionConfig + mount *nfs.Mount + target *nfs.Target +} + +func New(c *config.ConnectionConfig, secret string) (*NFSExplorer, error) { + if runtime.GOOS == "windows" { + return nil, fmt.Errorf("NFS not supported on this platform") + } + return &NFSExplorer{cfg: c}, nil +} + +func (e *NFSExplorer) Connect() error { + mount, err := nfs.DialMount(e.cfg.Host) + if err != nil { + return err + } + e.mount = mount + + auth := rpc.AuthNull + target, err := mount.Mount(e.cfg.Bucket, auth) + if err != nil { + mount.Close() + return err + } + e.target = target + return nil +} + +func (e *NFSExplorer) Disconnect() error { + if e.target != nil { + e.target.Close() + } + if e.mount != nil { + e.mount.Close() + } + return nil +} + +func (e *NFSExplorer) ListDir(path string) ([]explorer.FileEntry, error) { + if path == "" { + path = "." + } + files, err := e.target.ReadDirPlus(path) + if err != nil { + return nil, err + } + + var entries []explorer.FileEntry + for _, f := range files { + if f.Name() == "." || f.Name() == ".." { + continue + } + entries = append(entries, explorer.FileEntry{ + Name: f.Name(), + Path: filepath.ToSlash(filepath.Join(path, f.Name())), + Size: f.Size(), + Modified: f.ModTime().Format(time.RFC3339), + IsDir: f.IsDir(), + Permissions: f.Mode().String(), + }) + } + return entries, nil +} + +func (e *NFSExplorer) Stat(path string) (explorer.FileEntry, error) { + if path == "" { + path = "." + } + f, _, err := e.target.Lookup(path) + if err != nil { + return explorer.FileEntry{}, err + } + return explorer.FileEntry{ + Name: f.Name(), + Path: filepath.ToSlash(path), + Size: f.Size(), + Modified: f.ModTime().Format(time.RFC3339), + IsDir: f.IsDir(), + Permissions: f.Mode().String(), + }, nil +} + +func (e *NFSExplorer) MkDir(path string) error { + _, err := e.target.Mkdir(path, 0755) + return err +} + +func (e *NFSExplorer) Delete(path string) error { + return e.target.RemoveAll(path) +} + +func (e *NFSExplorer) Rename(src, dst string) error { + return fmt.Errorf("rename not fully supported by go-nfs-client wrapper") +} + +func (e *NFSExplorer) ReadFile(path string) (io.ReadCloser, error) { + return e.target.Open(path) +} + +func (e *NFSExplorer) WriteFile(path string, r io.Reader, size int64) error { + f, err := e.target.OpenFile(path, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, r) + return err +} diff --git a/internal/protocols/s3/s3.go b/internal/protocols/s3/s3.go new file mode 100644 index 0000000..33b609f --- /dev/null +++ b/internal/protocols/s3/s3.go @@ -0,0 +1,309 @@ +package s3 + +import ( + "context" + "fmt" + "io" + "path/filepath" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + appcfg "goexplore/internal/config" + "goexplore/internal/explorer" +) + +type S3Explorer struct { + cfg *appcfg.ConnectionConfig + secret string + client *s3.Client +} + +func New(c *appcfg.ConnectionConfig, secret string) *S3Explorer { + return &S3Explorer{cfg: c, secret: secret} +} + +func (e *S3Explorer) Connect() error { + region := e.cfg.Region + if region == "" { + region = "us-east-1" + } + + creds := credentials.NewStaticCredentialsProvider(e.cfg.Username, e.secret, "") + + optFns := []func(*config.LoadOptions) error{ + config.WithCredentialsProvider(creds), + config.WithRegion(region), + } + + awsCfg, err := config.LoadDefaultConfig(context.TODO(), optFns...) + if err != nil { + return err + } + + clientOpts := func(o *s3.Options) { + if e.cfg.Host != "" { + host := e.cfg.Host + if !strings.HasPrefix(host, "http") { + host = "https://" + host + } + if e.cfg.Port > 0 { + host = fmt.Sprintf("%s:%d", host, e.cfg.Port) + } + o.BaseEndpoint = aws.String(host) + } + o.UsePathStyle = e.cfg.PathStyle + } + + e.client = s3.NewFromConfig(awsCfg, clientOpts) + return nil +} + +func (e *S3Explorer) Disconnect() error { + return nil +} + +func (e *S3Explorer) getBucketAndPath(path string) (string, string, string, error) { + if e.cfg.Bucket != "" { + return e.cfg.Bucket, strings.TrimPrefix(path, "/"), "", nil + } + + path = strings.TrimPrefix(path, "/") + if path == "" { + return "", "", "", fmt.Errorf("no bucket specified") + } + + parts := strings.SplitN(path, "/", 2) + bucket := parts[0] + subpath := "" + if len(parts) > 1 { + subpath = parts[1] + } + return bucket, subpath, bucket, nil +} + +func (e *S3Explorer) ListDir(path string) ([]explorer.FileEntry, error) { + if e.cfg.Bucket == "" && (path == "" || path == "/") { + result, err := e.client.ListBuckets(context.TODO(), &s3.ListBucketsInput{}) + if err != nil { + return nil, err + } + var entries []explorer.FileEntry + for _, b := range result.Buckets { + entries = append(entries, explorer.FileEntry{ + Name: *b.Name, + Path: *b.Name, + Modified: b.CreationDate.Format(time.RFC3339), + IsDir: true, + Permissions: "bucket", + }) + } + return entries, nil + } + + bucket, subpath, bucketName, err := e.getBucketAndPath(path) + if err != nil { + return nil, err + } + + prefix := subpath + if prefix != "" && !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + + input := &s3.ListObjectsV2Input{ + Bucket: aws.String(bucket), + Prefix: aws.String(prefix), + Delimiter: aws.String("/"), + } + + result, err := e.client.ListObjectsV2(context.TODO(), input) + if err != nil { + return nil, err + } + + var entries []explorer.FileEntry + for _, cp := range result.CommonPrefixes { + name := strings.TrimSuffix(*cp.Prefix, "/") + name = filepath.Base(name) + + fullPath := *cp.Prefix + if e.cfg.Bucket == "" { + fullPath = bucketName + "/" + *cp.Prefix + } + + entries = append(entries, explorer.FileEntry{ + Name: name, + Path: fullPath, + IsDir: true, + Permissions: "dir", + }) + } + + for _, obj := range result.Contents { + if *obj.Key == prefix { + continue + } + name := filepath.Base(*obj.Key) + + fullPath := *obj.Key + if e.cfg.Bucket == "" { + fullPath = bucketName + "/" + *obj.Key + } + + entries = append(entries, explorer.FileEntry{ + Name: name, + Path: fullPath, + Size: *obj.Size, + Modified: obj.LastModified.Format(time.RFC3339), + IsDir: false, + Permissions: "file", + }) + } + return entries, nil +} + +func (e *S3Explorer) Stat(path string) (explorer.FileEntry, error) { + if e.cfg.Bucket == "" && (path == "" || path == "/") { + return explorer.FileEntry{ + Name: "/", Path: "/", IsDir: true, + }, nil + } + + bucket, subpath, bucketName, err := e.getBucketAndPath(path) + if err != nil { + return explorer.FileEntry{}, err + } + + if subpath == "" { + return explorer.FileEntry{ + Name: bucket, Path: bucketName, IsDir: true, + }, nil + } + + input := &s3.HeadObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(subpath), + } + head, err := e.client.HeadObject(context.TODO(), input) + if err != nil { + return explorer.FileEntry{ + Name: filepath.Base(subpath), + Path: path, + IsDir: true, + }, nil + } + + return explorer.FileEntry{ + Name: filepath.Base(subpath), + Path: path, + Size: *head.ContentLength, + Modified: head.LastModified.Format(time.RFC3339), + IsDir: false, + }, nil +} + +func (e *S3Explorer) MkDir(path string) error { + bucket, subpath, _, err := e.getBucketAndPath(path) + if err != nil { + return err + } + + if subpath == "" { + _, err := e.client.CreateBucket(context.TODO(), &s3.CreateBucketInput{ + Bucket: aws.String(bucket), + }) + return err + } + + if !strings.HasSuffix(subpath, "/") { + subpath += "/" + } + _, err = e.client.PutObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(subpath), + Body: strings.NewReader(""), + }) + return err +} + +func (e *S3Explorer) Delete(path string) error { + bucket, subpath, _, err := e.getBucketAndPath(path) + if err != nil { + return err + } + + if subpath == "" { + _, err := e.client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{ + Bucket: aws.String(bucket), + }) + return err + } + + _, err = e.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(subpath), + }) + return err +} + +func (e *S3Explorer) Rename(src, dst string) error { + bucket1, subpath1, _, err := e.getBucketAndPath(src) + if err != nil { + return err + } + bucket2, subpath2, _, err := e.getBucketAndPath(dst) + if err != nil { + return err + } + + if bucket1 != bucket2 { + return fmt.Errorf("cross-bucket rename not supported") + } + + _, err = e.client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucket2), + CopySource: aws.String(fmt.Sprintf("%s/%s", bucket1, subpath1)), + Key: aws.String(subpath2), + }) + if err != nil { + return err + } + + _, err = e.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ + Bucket: aws.String(bucket1), + Key: aws.String(subpath1), + }) + return err +} + +func (e *S3Explorer) ReadFile(path string) (io.ReadCloser, error) { + bucket, subpath, _, err := e.getBucketAndPath(path) + if err != nil { + return nil, err + } + out, err := e.client.GetObject(context.TODO(), &s3.GetObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(subpath), + }) + if err != nil { + return nil, err + } + return out.Body, nil +} + +func (e *S3Explorer) WriteFile(path string, r io.Reader, size int64) error { + bucket, subpath, _, err := e.getBucketAndPath(path) + if err != nil { + return err + } + _, err = e.client.PutObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(subpath), + Body: r, + }) + return err +} diff --git a/internal/protocols/sftp/sftp.go b/internal/protocols/sftp/sftp.go new file mode 100644 index 0000000..1918ee9 --- /dev/null +++ b/internal/protocols/sftp/sftp.go @@ -0,0 +1,164 @@ +package sftp + +import ( + "fmt" + "io" + "path/filepath" + "time" + + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" + "goexplore/internal/config" + "goexplore/internal/explorer" +) + +type SFTPExplorer struct { + cfg *config.ConnectionConfig + secret string + client *sftp.Client + conn *ssh.Client +} + +func New(c *config.ConnectionConfig, secret string) *SFTPExplorer { + return &SFTPExplorer{cfg: c, secret: secret} +} + +func (e *SFTPExplorer) Connect() error { + port := e.cfg.Port + if port == 0 { + port = 22 + } + + var authMethod ssh.AuthMethod + signer, err := ssh.ParsePrivateKey([]byte(e.secret)) + if err == nil { + authMethod = ssh.PublicKeys(signer) + } else { + authMethod = ssh.Password(e.secret) + } + + config := &ssh.ClientConfig{ + User: e.cfg.Username, + Auth: []ssh.AuthMethod{ + authMethod, + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", e.cfg.Host, port), config) + if err != nil { + return err + } + e.conn = conn + + client, err := sftp.NewClient(conn) + if err != nil { + conn.Close() + return err + } + e.client = client + return nil +} + +func (e *SFTPExplorer) Disconnect() error { + if e.client != nil { + e.client.Close() + } + if e.conn != nil { + e.conn.Close() + } + return nil +} + +func (e *SFTPExplorer) ListDir(path string) ([]explorer.FileEntry, error) { + if path == "" { + path = "." + } + files, err := e.client.ReadDir(path) + if err != nil { + return nil, err + } + + var entries []explorer.FileEntry + for _, f := range files { + entries = append(entries, explorer.FileEntry{ + Name: f.Name(), + Path: filepath.ToSlash(filepath.Join(path, f.Name())), + Size: f.Size(), + Modified: f.ModTime().Format(time.RFC3339), + IsDir: f.IsDir(), + Permissions: f.Mode().String(), + }) + } + return entries, nil +} + +func (e *SFTPExplorer) Stat(path string) (explorer.FileEntry, error) { + if path == "" { + path = "." + } + f, err := e.client.Stat(path) + if err != nil { + return explorer.FileEntry{}, err + } + return explorer.FileEntry{ + Name: f.Name(), + Path: filepath.ToSlash(path), + Size: f.Size(), + Modified: f.ModTime().Format(time.RFC3339), + IsDir: f.IsDir(), + Permissions: f.Mode().String(), + }, nil +} + +func (e *SFTPExplorer) MkDir(path string) error { + return e.client.MkdirAll(path) +} + +func (e *SFTPExplorer) removeAll(path string) error { + fi, err := e.client.Stat(path) + if err != nil { + return err + } + if !fi.IsDir() { + return e.client.Remove(path) + } + + files, err := e.client.ReadDir(path) + if err != nil { + return err + } + + for _, f := range files { + if f.Name() == "." || f.Name() == ".." { + continue + } + if err := e.removeAll(filepath.Join(path, f.Name())); err != nil { + return err + } + } + + return e.client.RemoveDirectory(path) +} + +func (e *SFTPExplorer) Delete(path string) error { + return e.removeAll(path) +} + +func (e *SFTPExplorer) Rename(src, dst string) error { + return e.client.Rename(src, dst) +} + +func (e *SFTPExplorer) ReadFile(path string) (io.ReadCloser, error) { + return e.client.Open(path) +} + +func (e *SFTPExplorer) WriteFile(path string, r io.Reader, size int64) error { + f, err := e.client.Create(path) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, r) + return err +} diff --git a/internal/protocols/smb/smb.go b/internal/protocols/smb/smb.go new file mode 100644 index 0000000..22c7fd3 --- /dev/null +++ b/internal/protocols/smb/smb.go @@ -0,0 +1,245 @@ +package smb + +import ( + "fmt" + "io" + "net" + "path/filepath" + "strings" + "time" + + "github.com/hirochachacha/go-smb2" + "goexplore/internal/config" + "goexplore/internal/explorer" +) + +type SMBExplorer struct { + cfg *config.ConnectionConfig + secret string + conn net.Conn + sess *smb2.Session + defaultShare *smb2.Share + shares map[string]*smb2.Share +} + +func New(c *config.ConnectionConfig, secret string) *SMBExplorer { + return &SMBExplorer{cfg: c, secret: secret, shares: make(map[string]*smb2.Share)} +} + +func (e *SMBExplorer) Connect() error { + port := e.cfg.Port + if port == 0 { + port = 445 + } + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", e.cfg.Host, port)) + if err != nil { + return err + } + e.conn = conn + + d := &smb2.Dialer{ + Initiator: &smb2.NTLMInitiator{ + User: e.cfg.Username, + Password: e.secret, + }, + } + + s, err := d.Dial(conn) + if err != nil { + conn.Close() + return err + } + e.sess = s + + if e.cfg.Bucket != "" { + share, err := s.Mount(e.cfg.Bucket) + if err != nil { + s.Logoff() + conn.Close() + return err + } + e.defaultShare = share + } + + return nil +} + +func (e *SMBExplorer) Disconnect() error { + if e.defaultShare != nil { + e.defaultShare.Umount() + } + for _, s := range e.shares { + s.Umount() + } + if e.sess != nil { + e.sess.Logoff() + } + if e.conn != nil { + e.conn.Close() + } + return nil +} + +func (e *SMBExplorer) getShareAndPath(path string) (*smb2.Share, string, string, error) { + if e.defaultShare != nil { + if path == "" || path == "/" { + path = "." + } + return e.defaultShare, strings.ReplaceAll(path, "/", "\\"), "", nil + } + + path = strings.TrimPrefix(filepath.ToSlash(path), "/") + if path == "" || path == "." { + return nil, "", "", fmt.Errorf("no share specified") + } + + parts := strings.SplitN(path, "/", 2) + sharename := parts[0] + subpath := "." + if len(parts) > 1 { + subpath = parts[1] + } + + if s, ok := e.shares[sharename]; ok { + return s, strings.ReplaceAll(subpath, "/", "\\"), sharename, nil + } + + s, err := e.sess.Mount(sharename) + if err != nil { + return nil, "", "", err + } + e.shares[sharename] = s + return s, strings.ReplaceAll(subpath, "/", "\\"), sharename, nil +} + +func (e *SMBExplorer) ListDir(path string) ([]explorer.FileEntry, error) { + if e.defaultShare == nil && (path == "" || path == "/" || path == ".") { + shares, err := e.sess.ListSharenames() + if err != nil { + return nil, err + } + var entries []explorer.FileEntry + for _, s := range shares { + entries = append(entries, explorer.FileEntry{ + Name: s, + Path: s, + IsDir: true, + Permissions: "share", + }) + } + return entries, nil + } + + share, subpath, shareName, err := e.getShareAndPath(path) + if err != nil { + return nil, err + } + + files, err := share.ReadDir(subpath) + if err != nil { + return nil, err + } + + var entries []explorer.FileEntry + for _, f := range files { + fullPath := "" + if e.defaultShare != nil { + fullPath = strings.ReplaceAll(filepath.Join(subpath, f.Name()), "\\", "/") + } else { + fullPath = filepath.ToSlash(filepath.Join(shareName, subpath, f.Name())) + } + entries = append(entries, explorer.FileEntry{ + Name: f.Name(), + Path: fullPath, + Size: f.Size(), + Modified: f.ModTime().Format(time.RFC3339), + IsDir: f.IsDir(), + Permissions: f.Mode().String(), + }) + } + return entries, nil +} + +func (e *SMBExplorer) Stat(path string) (explorer.FileEntry, error) { + if e.defaultShare == nil && (path == "" || path == "/" || path == ".") { + return explorer.FileEntry{ + Name: "/", Path: "/", IsDir: true, Permissions: "root", + }, nil + } + + share, subpath, shareName, err := e.getShareAndPath(path) + if err != nil { + return explorer.FileEntry{}, err + } + + f, err := share.Stat(subpath) + if err != nil { + return explorer.FileEntry{}, err + } + + fullPath := strings.ReplaceAll(subpath, "\\", "/") + if e.defaultShare == nil { + fullPath = filepath.ToSlash(filepath.Join(shareName, subpath)) + } + return explorer.FileEntry{ + Name: f.Name(), + Path: fullPath, + Size: f.Size(), + Modified: f.ModTime().Format(time.RFC3339), + IsDir: f.IsDir(), + Permissions: f.Mode().String(), + }, nil +} + +func (e *SMBExplorer) MkDir(path string) error { + share, subpath, _, err := e.getShareAndPath(path) + if err != nil { + return err + } + return share.Mkdir(subpath, 0755) +} + +func (e *SMBExplorer) Delete(path string) error { + share, subpath, _, err := e.getShareAndPath(path) + if err != nil { + return err + } + return share.RemoveAll(subpath) +} + +func (e *SMBExplorer) Rename(src, dst string) error { + share1, subpath1, shareName1, err := e.getShareAndPath(src) + if err != nil { + return err + } + _, subpath2, shareName2, err := e.getShareAndPath(dst) + if err != nil { + return err + } + if shareName1 != shareName2 { + return fmt.Errorf("cross-share rename not supported") + } + return share1.Rename(subpath1, subpath2) +} + +func (e *SMBExplorer) ReadFile(path string) (io.ReadCloser, error) { + share, subpath, _, err := e.getShareAndPath(path) + if err != nil { + return nil, err + } + return share.Open(subpath) +} + +func (e *SMBExplorer) WriteFile(path string, r io.Reader, size int64) error { + share, subpath, _, err := e.getShareAndPath(path) + if err != nil { + return err + } + f, err := share.Create(subpath) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, r) + return err +} diff --git a/internal/protocols/webdav/webdav.go b/internal/protocols/webdav/webdav.go new file mode 100644 index 0000000..5ecb3bf --- /dev/null +++ b/internal/protocols/webdav/webdav.go @@ -0,0 +1,104 @@ +package webdav + +import ( + "fmt" + "io" + "strings" + "time" + + "github.com/studio-b12/gowebdav" + "goexplore/internal/config" + "goexplore/internal/explorer" +) + +type WebDAVExplorer struct { + cfg *config.ConnectionConfig + secret string + client *gowebdav.Client +} + +func New(c *config.ConnectionConfig, secret string) *WebDAVExplorer { + return &WebDAVExplorer{cfg: c, secret: secret} +} + +func (e *WebDAVExplorer) Connect() error { + host := e.cfg.Host + if !strings.HasPrefix(host, "http") { + host = "http://" + host + } + if e.cfg.Port > 0 { + host = fmt.Sprintf("%s:%d", host, e.cfg.Port) + } + if e.cfg.Bucket != "" { + host = host + "/" + e.cfg.Bucket + } + + e.client = gowebdav.NewClient(host, e.cfg.Username, e.secret) + err := e.client.Connect() + if err != nil { + return err + } + return nil +} + +func (e *WebDAVExplorer) Disconnect() error { + return nil +} + +func (e *WebDAVExplorer) ListDir(path string) ([]explorer.FileEntry, error) { + if path == "" { + path = "/" + } + files, err := e.client.ReadDir(path) + if err != nil { + return nil, err + } + + var entries []explorer.FileEntry + for _, f := range files { + entries = append(entries, explorer.FileEntry{ + Name: f.Name(), + Path: strings.TrimSuffix(path, "/") + "/" + f.Name(), + Size: f.Size(), + Modified: f.ModTime().Format(time.RFC3339), + IsDir: f.IsDir(), + Permissions: f.Mode().String(), + }) + } + return entries, nil +} + +func (e *WebDAVExplorer) Stat(path string) (explorer.FileEntry, error) { + f, err := e.client.Stat(path) + if err != nil { + return explorer.FileEntry{}, err + } + return explorer.FileEntry{ + Name: f.Name(), + Path: path, + Size: f.Size(), + Modified: f.ModTime().Format(time.RFC3339), + IsDir: f.IsDir(), + Permissions: f.Mode().String(), + }, nil +} + +func (e *WebDAVExplorer) MkDir(path string) error { + return e.client.MkdirAll(path, 0755) +} + +func (e *WebDAVExplorer) Delete(path string) error { + return e.client.RemoveAll(path) +} + +func (e *WebDAVExplorer) Rename(src, dst string) error { + return e.client.Rename(src, dst, true) +} + +func (e *WebDAVExplorer) ReadFile(path string) (io.ReadCloser, error) { + return e.client.ReadStream(path) +} + +func (e *WebDAVExplorer) WriteFile(path string, r io.Reader, size int64) error { + return e.client.WriteStream(path, r, 0644) +} diff --git a/internal/transfer/transfer.go b/internal/transfer/transfer.go new file mode 100644 index 0000000..7d852aa --- /dev/null +++ b/internal/transfer/transfer.go @@ -0,0 +1,144 @@ +package transfer + +import ( + "errors" + "io" + "sync" + "time" + + "goexplore/internal/explorer" +) + +type Status string + +const ( + StatusQueued Status = "queued" + StatusActive Status = "active" + StatusComplete Status = "complete" + StatusFailed Status = "failed" +) + +type Transfer struct { + ID string `json:"id"` + Source string `json:"source"` + Destination string `json:"destination"` + Filename string `json:"filename"` + BytesTotal int64 `json:"bytes_total"` + BytesDone int64 `json:"bytes_done"` + SpeedMBps float64 `json:"speed_mbps"` + ETA int `json:"eta_seconds"` + Status Status `json:"status"` + Error string `json:"error,omitempty"` + + srcExp explorer.Explorer + dstExp explorer.Explorer + + startTime time.Time +} + +type Manager struct { + mu sync.Mutex + transfers map[string]*Transfer + queue chan *Transfer + concurrency int +} + +func NewManager(concurrency int) *Manager { + m := &Manager{ + transfers: make(map[string]*Transfer), + queue: make(chan *Transfer, 100), + concurrency: concurrency, + } + for i := 0; i < concurrency; i++ { + go m.worker() + } + return m +} + +func (m *Manager) worker() { + for t := range m.queue { + m.process(t) + } +} + +func (m *Manager) process(t *Transfer) { + t.Status = StatusActive + t.startTime = time.Now() + err := m.doTransfer(t) + if err != nil { + t.Status = StatusFailed + t.Error = err.Error() + } else { + t.Status = StatusComplete + t.BytesDone = t.BytesTotal + } +} + +func (m *Manager) doTransfer(t *Transfer) error { + r, err := t.srcExp.ReadFile(t.Source) + if err != nil { + return err + } + defer r.Close() + + pr := &progressReader{ + r: r, + t: t, + } + + return t.dstExp.WriteFile(t.Destination, pr, t.BytesTotal) +} + +type progressReader struct { + r io.Reader + t *Transfer +} + +func (pr *progressReader) Read(p []byte) (int, error) { + n, err := pr.r.Read(p) + if n > 0 { + pr.t.BytesDone += int64(n) + elapsed := time.Since(pr.t.startTime).Seconds() + if elapsed > 0 { + pr.t.SpeedMBps = (float64(pr.t.BytesDone) / 1024 / 1024) / elapsed + if pr.t.SpeedMBps > 0 { + pr.t.ETA = int(float64(pr.t.BytesTotal-pr.t.BytesDone) / 1024 / 1024 / pr.t.SpeedMBps) + } + } + } + return n, err +} + +func (m *Manager) QueueTransfer(id, srcPath, dstPath, filename string, size int64, srcExp, dstExp explorer.Explorer) error { + if srcExp == nil || dstExp == nil { + return errors.New("invalid explorers") + } + + t := &Transfer{ + ID: id, + Source: srcPath, + Destination: dstPath, + Filename: filename, + BytesTotal: size, + Status: StatusQueued, + srcExp: srcExp, + dstExp: dstExp, + } + + m.mu.Lock() + m.transfers[id] = t + m.mu.Unlock() + + m.queue <- t + return nil +} + +func (m *Manager) GetTransfers() []*Transfer { + m.mu.Lock() + defer m.mu.Unlock() + res := make([]*Transfer, 0, len(m.transfers)) + for _, t := range m.transfers { + res = append(res, t) + } + return res +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..df49852 --- /dev/null +++ b/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "embed" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/options/linux" +) + +//go:embed all:frontend/dist +var assets embed.FS + +//go:embed build/appicon.png +var icon []byte + +func main() { + // Create an instance of the app structure + app := NewApp() + + // Create application with options + err := wails.Run(&options.App{ + Title: "goexplore", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 10, G: 10, B: 10, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + Linux: &linux.Options{ + Icon: icon, + }, + }) + + if err != nil { + println("Error:", err.Error()) + } +} diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..ed8a774 --- /dev/null +++ b/wails.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "goexplore", + "outputfilename": "goexplore-init", + "frontend:install": "npm install", + "frontend:build": "npm run build", + "frontend:dev:watcher": "npm run dev", + "frontend:dev:serverUrl": "auto", + "author": { + "name": "Jamie Banks", + "email": "jamie@jdbnet.co.uk" + } +}