diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 008808d..4b1ea54 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,11 +12,11 @@ jobs:
- personal
- business
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- - uses: actions/setup-go@v2
+ - uses: actions/setup-go@v5
with:
- go-version: "1.18"
+ go-version: "1.19"
- name: Install apt dependencies
run: |
@@ -33,7 +33,7 @@ jobs:
libreoffice
sudo rm /usr/local/bin/aws # whyyy
- - uses: actions/cache@v2
+ - uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
@@ -63,12 +63,12 @@ jobs:
# cannot run systemd tests here because github actions runners don't have dbus setup +
# if CGO is on, the UI tests will take foreverrrrr
bash cgo-helper.sh
- CGO_ENABLED=0 gotest -v -covermode=count -coverpkg=./ui/... -coverprofile=ui.coverage ./ui
- gotest -v -covermode=count -coverpkg=./cmd/common -coverprofile=common.coverage ./cmd/common
- gotest -v -covermode=count -coverpkg=./fs/... -coverprofile=quickxorhash.coverage ./fs/graph/quickxorhash
- gotest -v -covermode=count -coverpkg=./fs/... -coverprofile=graph.coverage ./fs/graph
- gotest -v -covermode=count -coverpkg=./fs/... -coverprofile=fs.coverage ./fs
- go test -c -covermode=count -coverpkg=./fs/... ./fs/offline
+ CGO_ENABLED=0 gotest -v -tags=glib_2_64 -covermode=count -coverpkg=./ui/... -coverprofile=ui.coverage ./ui
+ gotest -v -tags=glib_2_64 -covermode=count -coverpkg=./cmd/common -coverprofile=common.coverage ./cmd/common
+ gotest -v -tags=glib_2_64 -covermode=count -coverpkg=./fs/... -coverprofile=quickxorhash.coverage ./fs/graph/quickxorhash
+ gotest -v -tags=glib_2_64 -covermode=count -coverpkg=./fs/... -coverprofile=graph.coverage ./fs/graph
+ gotest -v -tags=glib_2_64 -covermode=count -coverpkg=./fs/... -coverprofile=fs.coverage ./fs
+ go test -c -tags=glib_2_64 -covermode=count -coverpkg=./fs/... ./fs/offline
sudo unshare -n -S $(id -u) -G $(id -g) ./offline.test -test.v -test.coverprofile=offline.coverage
- name: Copy new auth tokens to S3
@@ -92,7 +92,7 @@ jobs:
if: always()
- name: Send test coverage to Coveralls
- uses: coverallsapp/github-action@v1.1.2
+ uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: coverage.lcov
@@ -108,7 +108,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Coveralls finished
- uses: coverallsapp/github-action@v1.1.2
+ uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
diff --git a/cmd/common/common.go b/cmd/common/common.go
index 4dbdd76..94d2bc3 100644
--- a/cmd/common/common.go
+++ b/cmd/common/common.go
@@ -2,7 +2,11 @@
package common
import (
+ "errors"
"fmt"
+ "io/ioutil"
+ "os"
+ "regexp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -35,3 +39,27 @@ func StringToLevel(input string) zerolog.Level {
func LogLevels() []string {
return []string{"trace", "debug", "info", "warn", "error", "fatal"}
}
+
+// TemplateXDGVolumeInfo returns
+func TemplateXDGVolumeInfo(name string) string {
+ xdgVolumeInfo := fmt.Sprintf("[Volume Info]\nName=%s\n", name)
+ if _, err := os.Stat("/usr/share/icons/onedriver/onedriver.png"); err == nil {
+ xdgVolumeInfo += "IconFile=/usr/share/icons/onedriver/onedriver.png\n"
+ }
+ return xdgVolumeInfo
+}
+
+// GetXDGVolumeInfoName returns the name of the drive according to whatever the
+// user has named it.
+func GetXDGVolumeInfoName(path string) (string, error) {
+ contents, err := ioutil.ReadFile(path)
+ if err != nil {
+ return "", err
+ }
+ regex := regexp.MustCompile("Name=(.*)")
+ name := regex.FindString(string(contents))
+ if len(name) < 5 {
+ return "", errors.New("could not find \"Name=\" key")
+ }
+ return name[5:], nil
+}
diff --git a/cmd/common/common_test.go b/cmd/common/common_test.go
new file mode 100644
index 0000000..0568676
--- /dev/null
+++ b/cmd/common/common_test.go
@@ -0,0 +1,20 @@
+package common
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// Write a sample .xdg-volume-info file and check that it can be read.
+func TestXDGVolumeInfo(t *testing.T) {
+ const expected = "some-volume name *()! $"
+ content := TemplateXDGVolumeInfo(expected)
+ file, _ := os.CreateTemp("", "onedriver-test-*")
+ os.WriteFile(file.Name(), []byte(content), 0600)
+ driveName, err := GetXDGVolumeInfoName(file.Name())
+ require.NoError(t, err)
+ assert.Equal(t, expected, driveName)
+}
diff --git a/cmd/onedriver-launcher/main.go b/cmd/onedriver-launcher/main.go
index 3a33f59..7882ffa 100644
--- a/cmd/onedriver-launcher/main.go
+++ b/cmd/onedriver-launcher/main.go
@@ -9,6 +9,7 @@ import "C"
import (
"fmt"
+ "io/ioutil"
"os"
"path/filepath"
"unsafe"
@@ -57,6 +58,9 @@ func main() {
os.Exit(0)
}
+ // loading config can emit an unformatted log message, so we do this first
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"})
+
// command line options override config options
config := common.LoadConfig(*configPath)
if *cacheDir != "" {
@@ -66,7 +70,6 @@ func main() {
config.LogLevel = *logLevel
}
- log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"})
zerolog.SetGlobalLevel(common.StringToLevel(config.LogLevel))
log.Info().Msgf("onedriver-launcher %s", common.Version())
@@ -242,55 +245,137 @@ func newMountRow(config common.Config, mount string) (*gtk.ListBoxRow, *gtk.Swit
escapedMount := unit.UnitNamePathEscape(mount)
unitName := systemd.TemplateUnit(systemd.OnedriverServiceTemplate, escapedMount)
- var label *gtk.Label
- tildePath := ui.EscapeHome(mount)
- accountName, err := ui.GetAccountName(config.CacheDir, escapedMount)
+ driveName, err := common.GetXDGVolumeInfoName(filepath.Join(mount, ".xdg-volume-info"))
if err != nil {
log.Error().
Err(err).
Str("mountpoint", mount).
- Msg("Could not determine acccount name.")
- label, _ = gtk.LabelNew(tildePath)
- } else {
+ Msg("Could not determine user-specified acccount name.")
+ }
+
+ tildePath := ui.EscapeHome(mount)
+ accountName, err := ui.GetAccountName(config.CacheDir, escapedMount)
+ label, _ := gtk.LabelNew("")
+ if driveName != "" {
+ // we have a user-assigned name for the user's drive
+ label.SetMarkup(fmt.Sprintf("%s (%s) ",
+ driveName, tildePath,
+ ))
+ } else if err == nil {
+ // fs isn't mounted, so just use user principal name from AAD
label, _ = gtk.LabelNew("")
label.SetMarkup(fmt.Sprintf("%s (%s) ",
accountName, tildePath,
))
+ } else {
+ // something went wrong and all we have is the mountpoint name
+ log.Error().
+ Err(err).
+ Str("mountpoint", mount).
+ Msg("Could not determine user principal name.")
+ label, _ = gtk.LabelNew(tildePath)
}
box.PackStart(label, false, false, 5)
- // create a button to delete the mountpoint
- deleteMountpointBtn, _ := gtk.ButtonNewFromIconName("user-trash-symbolic", gtk.ICON_SIZE_BUTTON)
- deleteMountpointBtn.SetTooltipText("Remove OneDrive account from local computer")
- deleteMountpointBtn.Connect("clicked", func() {
- log.Trace().
- Str("signal", "clicked").
+ // a switch to start/stop the mountpoint
+ mountToggle, _ := gtk.SwitchNew()
+ active, err := systemd.UnitIsActive(unitName)
+ if err == nil {
+ mountToggle.SetActive(active)
+ } else {
+ log.Error().Err(err).Msg("Error checking unit active state.")
+ }
+ mountToggle.SetTooltipText("Mount or unmount selected OneDrive account")
+ mountToggle.SetVAlign(gtk.ALIGN_CENTER)
+ mountToggle.Connect("state-set", func() {
+ log.Info().
+ Str("signal", "state-set").
Str("mount", mount).
Str("unitName", unitName).
- Msg("Request to delete mount.")
+ Bool("active", mountToggle.GetActive()).
+ Msg("Changing systemd unit active state.")
+ err := systemd.UnitSetActive(unitName, mountToggle.GetActive())
+ if err != nil {
+ log.Error().
+ Err(err).
+ Str("unit", unitName).
+ Msg("Could not change systemd unit active state.")
+ }
+ })
- if ui.CancelDialog("Remove mountpoint?", nil) {
- log.Info().
- Str("signal", "clicked").
- Str("mount", mount).
- Str("unitName", unitName).
- Msg("Deleting mount.")
- systemd.UnitSetEnabled(unitName, false)
- systemd.UnitSetActive(unitName, false)
+ mountpointSettingsBtn, _ := gtk.MenuButtonNew()
+ icon, _ := gtk.ImageNewFromIconName("emblem-system-symbolic", gtk.ICON_SIZE_BUTTON)
+ mountpointSettingsBtn.SetImage(icon)
+ popover, _ := gtk.PopoverNew(mountpointSettingsBtn)
+ mountpointSettingsBtn.SetPopover(popover)
+ popover.SetBorderWidth(8)
+ popoverBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 5)
- cachedir, _ := os.UserCacheDir()
- os.RemoveAll(fmt.Sprintf("%s/onedriver/%s/", cachedir, escapedMount))
+ if accountName != "" {
+ accountLabel, _ := gtk.LabelNew(accountName)
+ popoverBox.Add(accountLabel)
+ }
+ // rename the mount by rewriting the .xdg-volume-info file
+ renameMountpointEntry, _ := gtk.EntryNew()
+ renameMountpointEntry.SetTooltipText("Change the label that your file browser uses for this drive")
+ renameMountpointEntry.SetText(driveName)
+ // runs on enter
+ renameMountpointEntry.Connect("activate", func(entry *gtk.Entry) {
+ newName, err := entry.GetText()
+ ctx := log.With().
+ Str("signal", "clicked").
+ Str("mount", mount).
+ Str("unitName", unitName).
+ Str("oldName", driveName).
+ Str("newName", newName).
+ Logger()
+ if err != nil {
+ ctx.Error().Err(err).Msg("Failed to get new drive name.")
+ return
+ }
+ if driveName == newName {
+ ctx.Info().Msg("New name is same as old name, ignoring.")
+ return
+ }
+ ctx.Info().
+ Msg("Renaming mount.")
+ popover.GrabFocus()
- row.Destroy()
+ err = systemd.UnitSetActive(unitName, true)
+ if err != nil {
+ ctx.Error().Err(err).Msg("Failed to start mount for rename.")
+ return
}
+ mountToggle.SetActive(true)
+
+ if ui.PollUntilAvail(mount, -1) {
+ xdgVolumeInfo := common.TemplateXDGVolumeInfo(newName)
+ driveName = newName
+ //FIXME why does this not work???
+ err = ioutil.WriteFile(filepath.Join(mount, ".xdg-volume-info"), []byte(xdgVolumeInfo), 0644)
+ if err != nil {
+ ctx.Error().Err(err).Msg("Failed to write new mount name.")
+ return
+ }
+ } else {
+ ctx.Error().Err(err).Msg("Mount never became ready.")
+ }
+ // update label in UI now
+ label.SetMarkup(fmt.Sprintf("%s (%s) ",
+ newName, tildePath,
+ ))
+
+ ui.Dialog("Drive rename will take effect on next filesystem start.", gtk.MESSAGE_INFO, nil)
+ ctx.Info().Msg("Drive rename will take effect on next filesystem start.")
})
- box.PackEnd(deleteMountpointBtn, false, false, 0)
+ popoverBox.Add(renameMountpointEntry)
+
+ separator, _ := gtk.SeparatorMenuItemNew()
+ popoverBox.Add(separator)
// create a button to enable/disable the mountpoint
- unitEnabledBtn, _ := gtk.ToggleButtonNew()
- enabledImg, _ := gtk.ImageNewFromIconName("object-select-symbolic", gtk.ICON_SIZE_BUTTON)
- unitEnabledBtn.SetImage(enabledImg)
- unitEnabledBtn.SetTooltipText("Start mountpoint on login")
+ unitEnabledBtn, _ := gtk.CheckButtonNewWithLabel(" Start drive on login")
+ unitEnabledBtn.SetTooltipText("Start this drive automatically when you login")
enabled, err := systemd.UnitIsEnabled(unitName)
if err == nil {
unitEnabledBtn.SetActive(enabled)
@@ -312,33 +397,45 @@ func newMountRow(config common.Config, mount string) (*gtk.ListBoxRow, *gtk.Swit
Msg("Could not change systemd unit enabled state.")
}
})
- box.PackEnd(unitEnabledBtn, false, false, 0)
+ popoverBox.PackStart(unitEnabledBtn, false, true, 0)
- // a switch to start/stop the mountpoint
- mountToggle, _ := gtk.SwitchNew()
- active, err := systemd.UnitIsActive(unitName)
- if err == nil {
- mountToggle.SetActive(active)
- } else {
- log.Error().Err(err).Msg("Error checking unit active state.")
- }
- mountToggle.SetTooltipText("Mount or unmount selected OneDrive account")
- mountToggle.SetVAlign(gtk.ALIGN_CENTER)
- mountToggle.Connect("state-set", func() {
- log.Info().
- Str("signal", "state-set").
+ // button to delete the mount
+ deleteMountpointBtn, _ := gtk.ModelButtonNew()
+ deleteMountpointBtn.SetLabel("Remove drive")
+ deleteMountpointBtn.SetTooltipText("Remove OneDrive account from local computer")
+ deleteMountpointBtn.Connect("clicked", func(button *gtk.ModelButton) {
+ log.Trace().
+ Str("signal", "clicked").
Str("mount", mount).
Str("unitName", unitName).
- Bool("active", mountToggle.GetActive()).
- Msg("Changing systemd unit active state.")
- err := systemd.UnitSetActive(unitName, mountToggle.GetActive())
- if err != nil {
- log.Error().
- Err(err).
- Str("unit", unitName).
- Msg("Could not change systemd unit active state.")
+ Msg("Request to delete drive.")
+
+ if ui.CancelDialog(nil, "Remove drive?",
+ "This will remove all data for this drive from your local computer. "+
+ "It can also be used to \"reset\" the drive to its original state.") {
+ log.Info().
+ Str("signal", "clicked").
+ Str("mount", mount).
+ Str("unitName", unitName).
+ Msg("Deleting mount.")
+ systemd.UnitSetEnabled(unitName, false)
+ systemd.UnitSetActive(unitName, false)
+
+ cachedir, _ := os.UserCacheDir()
+ os.RemoveAll(fmt.Sprintf("%s/onedriver/%s/", cachedir, escapedMount))
+
+ row.Destroy()
}
})
+ popoverBox.PackStart(deleteMountpointBtn, false, true, 0)
+
+ // ok show everything in the mount settings menu
+ popoverBox.ShowAll()
+ popover.Add(popoverBox)
+ popover.SetPosition(gtk.POS_BOTTOM)
+
+ // add all widgets to row in the right order
+ box.PackEnd(mountpointSettingsBtn, false, false, 0)
box.PackEnd(mountToggle, false, false, 0)
// name is used by "row-activated" callback
@@ -388,7 +485,7 @@ func newSettingsWindow(config *common.Config, configPath string) {
oldPath, _ := button.GetLabel()
oldPath = ui.UnescapeHome(oldPath)
path := ui.DirChooser("Select an empty directory to use for storage")
- if !ui.CancelDialog("Remount all drives?", settingsWindow) {
+ if !ui.CancelDialog(settingsWindow, "Remount all drives?", "") {
return
}
log.Warn().
diff --git a/cmd/onedriver/main.go b/cmd/onedriver/main.go
index 1abbb39..647bf77 100644
--- a/cmd/onedriver/main.go
+++ b/cmd/onedriver/main.go
@@ -166,11 +166,7 @@ func xdgVolumeInfo(filesystem *fs.Filesystem, auth *graph.Auth) {
log.Error().Err(err).Msg("Could not create .xdg-volume-info")
return
}
-
- xdgVolumeInfo := fmt.Sprintf("[Volume Info]\nName=%s\n", user.UserPrincipalName)
- if _, err := os.Stat("/usr/share/icons/onedriver/onedriver.png"); err == nil {
- xdgVolumeInfo += "IconFile=/usr/share/icons/onedriver/onedriver.png\n"
- }
+ xdgVolumeInfo := common.TemplateXDGVolumeInfo(user.UserPrincipalName)
// just upload directly and shove it in the cache
// (since the fs isn't mounted yet)
diff --git a/go.mod b/go.mod
index fc77c60..91eea23 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/jstaf/onedriver
require (
github.com/coreos/go-systemd/v22 v22.3.2
github.com/godbus/dbus/v5 v5.0.6
- github.com/gotk3/gotk3 v0.6.1
+ github.com/gotk3/gotk3 v0.6.3
github.com/hanwen/go-fuse/v2 v2.4.2
github.com/imdario/mergo v0.3.13
github.com/rs/zerolog v1.26.1
diff --git a/go.sum b/go.sum
index 92c7390..65c2e9e 100644
--- a/go.sum
+++ b/go.sum
@@ -5,8 +5,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gotk3/gotk3 v0.6.1 h1:GJ400a0ecEEWrzjBvzBzH+pB/esEMIGdB9zPSmBdoeo=
-github.com/gotk3/gotk3 v0.6.1/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
+github.com/gotk3/gotk3 v0.6.2 h1:sx/PjaKfKULJPTPq8p2kn2ZbcNFxpOJqi4VLzMbEOO8=
+github.com/gotk3/gotk3 v0.6.2/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
+github.com/gotk3/gotk3 v0.6.3 h1:+Ke4WkM1TQUNOlM2TZH6szqknqo+zNbX3BZWVXjSHYw=
+github.com/gotk3/gotk3 v0.6.3/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
github.com/hanwen/go-fuse/v2 v2.4.2 h1:ujevavwvGMg4s1TTSGWqid0q7WHk0XC8EOzHtygnt9E=
github.com/hanwen/go-fuse/v2 v2.4.2/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
diff --git a/ui/widgets.go b/ui/widgets.go
index e433312..529700a 100644
--- a/ui/widgets.go
+++ b/ui/widgets.go
@@ -42,11 +42,18 @@ func Dialog(msg string, messageType gtk.MessageType, parentWindow gtk.IWindow) {
// CancelDialog creates a "Continue?" style message, and returns what the user
// selected
-func CancelDialog(title string, parentWindow gtk.IWindow) bool {
- dialog, _ := gtk.DialogNewWithButtons(title, parentWindow, gtk.DIALOG_MODAL,
- []interface{}{"Cancel", gtk.RESPONSE_CANCEL},
- []interface{}{"Continue", gtk.RESPONSE_ACCEPT},
+func CancelDialog(parentWindow gtk.IWindow, primaryText, secondaryText string) bool {
+ dialog := gtk.MessageDialogNew(
+ parentWindow,
+ gtk.DIALOG_MODAL,
+ gtk.MESSAGE_WARNING,
+ gtk.BUTTONS_OK_CANCEL,
+ "",
)
+ dialog.SetMarkup(primaryText)
+ if secondaryText != "" {
+ dialog.FormatSecondaryMarkup(secondaryText)
+ }
defer dialog.Destroy()
- return dialog.Run() == gtk.RESPONSE_ACCEPT
+ return dialog.Run() == gtk.RESPONSE_OK
}