github.com/jstaf/onedriver@v0.14.2-0.20240420231225-f07678f9e6ef/cmd/onedriver/main.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/signal"
     9  	"path/filepath"
    10  	"strings"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/coreos/go-systemd/v22/unit"
    15  	"github.com/hanwen/go-fuse/v2/fuse"
    16  	"github.com/jstaf/onedriver/cmd/common"
    17  	"github.com/jstaf/onedriver/fs"
    18  	"github.com/jstaf/onedriver/fs/graph"
    19  	"github.com/rs/zerolog"
    20  	"github.com/rs/zerolog/log"
    21  	flag "github.com/spf13/pflag"
    22  )
    23  
    24  func usage() {
    25  	fmt.Printf(`onedriver - A Linux client for Microsoft OneDrive.
    26  
    27  This program will mount your OneDrive account as a Linux filesystem at the
    28  specified mountpoint. Note that this is not a sync client - files are only
    29  fetched on-demand and cached locally. Only files you actually use will be
    30  downloaded. While offline, the filesystem will be read-only until
    31  connectivity is re-established.
    32  
    33  Usage: onedriver [options] <mountpoint>
    34  
    35  Valid options:
    36  `)
    37  	flag.PrintDefaults()
    38  }
    39  
    40  func main() {
    41  	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"})
    42  
    43  	// setup cli parsing
    44  	authOnly := flag.BoolP("auth-only", "a", false,
    45  		"Authenticate to OneDrive and then exit.")
    46  	headless := flag.BoolP("no-browser", "n", false,
    47  		"This disables launching the built-in web browser during authentication. "+
    48  			"Follow the instructions in the terminal to authenticate to OneDrive.")
    49  	configPath := flag.StringP("config-file", "f", common.DefaultConfigPath(),
    50  		"A YAML-formatted configuration file used by onedriver.")
    51  	logLevel := flag.StringP("log", "l", "",
    52  		"Set logging level/verbosity for the filesystem. "+
    53  			"Can be one of: fatal, error, warn, info, debug, trace")
    54  	cacheDir := flag.StringP("cache-dir", "c", "",
    55  		"Change the default cache directory used by onedriver. "+
    56  			"Will be created if the path does not already exist.")
    57  	wipeCache := flag.BoolP("wipe-cache", "w", false,
    58  		"Delete the existing onedriver cache directory and then exit. "+
    59  			"This is equivalent to resetting the program.")
    60  	versionFlag := flag.BoolP("version", "v", false, "Display program version.")
    61  	debugOn := flag.BoolP("debug", "d", false, "Enable FUSE debug logging. "+
    62  		"This logs communication between onedriver and the kernel.")
    63  	help := flag.BoolP("help", "h", false, "Displays this help message.")
    64  	flag.Usage = usage
    65  	flag.Parse()
    66  
    67  	if *help {
    68  		flag.Usage()
    69  		os.Exit(0)
    70  	}
    71  	if *versionFlag {
    72  		fmt.Println("onedriver", common.Version())
    73  		os.Exit(0)
    74  	}
    75  
    76  	config := common.LoadConfig(*configPath)
    77  	// command line options override config options
    78  	if *cacheDir != "" {
    79  		config.CacheDir = *cacheDir
    80  	}
    81  	if *logLevel != "" {
    82  		config.LogLevel = *logLevel
    83  	}
    84  
    85  	zerolog.SetGlobalLevel(common.StringToLevel(config.LogLevel))
    86  
    87  	// wipe cache if desired
    88  	if *wipeCache {
    89  		log.Info().Str("path", config.CacheDir).Msg("Removing cache.")
    90  		os.RemoveAll(config.CacheDir)
    91  		os.Exit(0)
    92  	}
    93  
    94  	// determine and validate mountpoint
    95  	if len(flag.Args()) == 0 {
    96  		flag.Usage()
    97  		fmt.Fprintf(os.Stderr, "\nNo mountpoint provided, exiting.\n")
    98  		os.Exit(1)
    99  	}
   100  
   101  	mountpoint := flag.Arg(0)
   102  	st, err := os.Stat(mountpoint)
   103  	if err != nil || !st.IsDir() {
   104  		log.Fatal().
   105  			Str("mountpoint", mountpoint).
   106  			Msg("Mountpoint did not exist or was not a directory.")
   107  	}
   108  	if res, _ := ioutil.ReadDir(mountpoint); len(res) > 0 {
   109  		log.Fatal().Str("mountpoint", mountpoint).Msg("Mountpoint must be empty.")
   110  	}
   111  
   112  	// compute cache name as systemd would
   113  	absMountPath, _ := filepath.Abs(mountpoint)
   114  	cachePath := filepath.Join(config.CacheDir, unit.UnitNamePathEscape(absMountPath))
   115  
   116  	// authenticate/re-authenticate if necessary
   117  	os.MkdirAll(cachePath, 0700)
   118  	authPath := filepath.Join(cachePath, "auth_tokens.json")
   119  	if *authOnly {
   120  		os.Remove(authPath)
   121  		graph.Authenticate(config.AuthConfig, authPath, *headless)
   122  		os.Exit(0)
   123  	}
   124  
   125  	// create the filesystem
   126  	log.Info().Msgf("onedriver %s", common.Version())
   127  	auth := graph.Authenticate(config.AuthConfig, authPath, *headless)
   128  	filesystem := fs.NewFilesystem(auth, cachePath)
   129  	go filesystem.DeltaLoop(30 * time.Second)
   130  	xdgVolumeInfo(filesystem, auth)
   131  
   132  	server, err := fuse.NewServer(filesystem, mountpoint, &fuse.MountOptions{
   133  		Name:          "onedriver",
   134  		FsName:        "onedriver",
   135  		DisableXAttrs: true,
   136  		MaxBackground: 1024,
   137  		Debug:         *debugOn,
   138  	})
   139  	if err != nil {
   140  		log.Fatal().Err(err).Msgf("Mount failed. Is the mountpoint already in use? "+
   141  			"(Try running \"fusermount3 -uz %s\")\n", mountpoint)
   142  	}
   143  
   144  	// setup signal handler for graceful unmount on signals like sigint
   145  	sigChan := make(chan os.Signal, 1)
   146  	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
   147  	go fs.UnmountHandler(sigChan, server)
   148  
   149  	// serve filesystem
   150  	log.Info().
   151  		Str("cachePath", cachePath).
   152  		Str("mountpoint", absMountPath).
   153  		Msg("Serving filesystem.")
   154  	server.Serve()
   155  }
   156  
   157  // xdgVolumeInfo createx .xdg-volume-info for a nice little onedrive logo in the
   158  // corner of the mountpoint and shows the account name in the nautilus sidebar
   159  func xdgVolumeInfo(filesystem *fs.Filesystem, auth *graph.Auth) {
   160  	if child, _ := filesystem.GetPath("/.xdg-volume-info", auth); child != nil {
   161  		return
   162  	}
   163  	log.Info().Msg("Creating .xdg-volume-info")
   164  	user, err := graph.GetUser(auth)
   165  	if err != nil {
   166  		log.Error().Err(err).Msg("Could not create .xdg-volume-info")
   167  		return
   168  	}
   169  	xdgVolumeInfo := common.TemplateXDGVolumeInfo(user.UserPrincipalName)
   170  
   171  	// just upload directly and shove it in the cache
   172  	// (since the fs isn't mounted yet)
   173  	resp, err := graph.Put(
   174  		graph.ResourcePath("/.xdg-volume-info")+":/content",
   175  		auth,
   176  		strings.NewReader(xdgVolumeInfo),
   177  	)
   178  	if err != nil {
   179  		log.Error().Err(err).Msg("Failed to write .xdg-volume-info")
   180  	}
   181  	root, _ := filesystem.GetPath("/", auth) // cannot fail
   182  	inode := fs.NewInode(".xdg-volume-info", 0644, root)
   183  	if json.Unmarshal(resp, &inode) == nil {
   184  		filesystem.InsertID(inode.ID(), inode)
   185  	}
   186  }