瀏覽代碼

Init cloud mount project with code from GDriveMount

Luis Figueiredo 7 年之前
當前提交
75d5065bec
共有 8 個文件被更改,包括 1357 次插入0 次删除
  1. 73 0
      README.md
  2. 59 0
      cmd/genversion/main.go
  3. 129 0
      fs/gdrivefs/client.go
  4. 293 0
      fs/gdrivefs/fileentry.go
  5. 655 0
      fs/gdrivefs/gdrive-fuse.go
  6. 10 0
      fs/gdrivefs/service.go
  7. 132 0
      main.go
  8. 6 0
      version.go

+ 73 - 0
README.md

@@ -0,0 +1,73 @@
+cloudmount
+=====================
+
+Linux util to Mount cloud drives
+
+Usage:
+```bash
+$ go get dev.hexasoftware.com/hxs/cloudmount
+$ go install dev.hexasoftware.com/hxs/cloudmount
+$
+
+```
+
+Support for:
+* Google Drive
+
+
+
+
+
+
+
+### Google Drive
+07-05-2017
+
+
+Setup Google client secrets:
+
+[https://console.developers.google.com/apis/credentials] (https://console.developers.google.com/apis/credentials)
+
+As of Google drive guidance:
+
+>	Turn on the Drive API
+
+>	1. Use [this wizard](https://console.developers.google.com/start/api?id=drive) to create or select a project in the Google Developers Console and automatically turn on the API. Click Continue, then Go to credentials.
+>	2. On the Add credentials to your project page, click the Cancel button.
+>	3. At the top of the page, select the OAuth consent screen tab. Select an Email address, enter a Product name if not already set, and click the Save button.
+>	4. Select the Credentials tab, click the Create credentials button and select OAuth client ID.
+>	5. Select the application type Other, enter the name "Drive API Quickstart", and click the Create button.
+>	6. Click OK to dismiss the resulting dialog.
+>	7. Click the file_download (Download JSON) button to the right of the client ID.
+
+Copy the downloaded JSON file to home directory as:    
+__$HOME/.gdrivemount/client_secret.json__   
+
+#### Signals
+Signal | Action                                                                                               | ex
+-------|------------------------------------------------------------------------------------------------------|-----------------
+USR1   | Refreshes directory tree from google drive                                                           | killall -USR1 gdrivemount
+HUP    | Perform a GC and shows memory usage <small>Works when its not running in daemon mode</small>         | killall -HUP gdrivemount
+
+
+
+
+
+####TODO:
+* Assure concurrency support on inode/handle creation for gdrive
+* Improve caching to refresh and save inodes
+* Reverse package structure instead of gdrivemount/cmd/gdrivemount use this as main package and move logic to subpackage
+* Use cloudmount -t gdrive -o uid, gid  MOUNTPOINT and add Support for other cloud services
+
+
+
+
+
+
+
+
+
+
+TODO:
+* create interface for drives
+* manager to handle drivers

+ 59 - 0
cmd/genversion/main.go

@@ -0,0 +1,59 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"regexp"
+	"strings"
+	"time"
+)
+
+func main() {
+	var pkg string
+	var dst string
+	flag.StringVar(&pkg, "package", "", "Set package name")
+	flag.StringVar(&dst, "out", "", "Output file")
+	flag.Parse()
+
+	if pkg == "" {
+		log.Println("Missing argument -package")
+		flag.Usage()
+		return
+	}
+	if dst == "" {
+		log.Println("Missing argument -out")
+		flag.Usage()
+		return
+	}
+
+	tag, err := exec.Command("git", "describe", "--tags").Output()
+	if err != nil {
+		log.Fatal("Error Getting tag", err)
+	}
+	re := regexp.MustCompile(`\r?\n`)
+	vtag := re.ReplaceAll(tag, []byte(" "))
+
+	version := fmt.Sprintf("%s - built: %s", strings.TrimSpace(string(vtag)), time.Now().UTC().Format("2006-01-02 15:04:05 UTC"))
+
+	fmt.Printf("Version: %s\n", version)
+
+	f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		fmt.Println("Error opening file", err)
+		return
+	}
+
+	fmt.Fprintf(f, "package %s\n", pkg)
+
+	fmt.Fprintln(f, "\nconst (")
+	fmt.Fprintf(f, "  //Version contains version of the package\n")
+	fmt.Fprintf(f, "  Version = \"%s\"\n", version)
+	fmt.Fprintln(f, ")")
+
+	f.Sync()
+	f.Close()
+
+}

+ 129 - 0
fs/gdrivefs/client.go

@@ -0,0 +1,129 @@
+package gdrivefs
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"os/user"
+	"path/filepath"
+
+	drive "google.golang.org/api/drive/v3"
+
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/google"
+)
+
+func getClient(ctx context.Context, config *oauth2.Config) *http.Client {
+	cacheFile, err := tokenCacheFile()
+	if err != nil {
+		log.Fatalf("Unable to get path to cached credential file. %v", err)
+	}
+
+	tok, err := tokenFromFile(cacheFile)
+	if err != nil {
+		tok = getTokenFromWeb(config)
+		saveToken(cacheFile, tok)
+	}
+	return config.Client(ctx, tok)
+
+}
+
+func tokenCacheFile() (string, error) {
+	tokenCacheDir, err := getConfigPath()
+	if err != nil {
+		return "", err
+	}
+
+	os.MkdirAll(tokenCacheDir, 0700)
+	return filepath.Join(tokenCacheDir, url.QueryEscape("auth.json")), err
+
+}
+
+func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
+	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
+
+	fmt.Printf(
+		`Go to the following link in your browser: 
+----------------------------------------------------------------------------------------------
+	%v
+----------------------------------------------------------------------------------------------
+
+type the authorization code: `, authURL)
+
+	var code string
+	if _, err := fmt.Scan(&code); err != nil {
+		log.Fatalf("Unable to read authorization code %v", err)
+	}
+
+	tok, err := config.Exchange(oauth2.NoContext, code)
+	if err != nil {
+		log.Fatalf("Unable to retrieve token from web: %v", err)
+	}
+
+	return tok
+}
+
+func tokenFromFile(file string) (*oauth2.Token, error) {
+	f, err := os.Open(file)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	t := &oauth2.Token{}
+	err = json.NewDecoder(f).Decode(t)
+	return t, err
+}
+
+func saveToken(file string, token *oauth2.Token) {
+	log.Printf("Saving credential file to: %s\n", file)
+	f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		log.Fatalf("Unable to cache oauth token: %v\n", err)
+	}
+	defer f.Close()
+
+	json.NewEncoder(f).Encode(token)
+}
+
+func getConfigPath() (string, error) {
+	usr, err := user.Current()
+	if err != nil {
+		return "", err
+	}
+	configDir := filepath.Join(usr.HomeDir, ".gdrivemount")
+
+	return configDir, nil
+}
+
+func GetDriveService() *drive.Service {
+
+	configPath, err := getConfigPath()
+	if err != nil {
+		log.Fatal("Unable to fetch config path")
+	}
+	ctx := context.Background()
+
+	b, err := ioutil.ReadFile(filepath.Join(configPath, "client_secret.json"))
+	if err != nil {
+		log.Fatalf("Unable to read client secret file: %v", err)
+	}
+
+	config, err := google.ConfigFromJSON(b, drive.DriveScope)
+	if err != nil {
+		log.Fatalf("Unable to parse client secret file: %v", err)
+	}
+
+	client := getClient(ctx, config)
+	srv, err := drive.New(client)
+	if err != nil {
+		log.Fatalf("Unable to retrieve drive Client: %v", err)
+	}
+
+	return srv
+
+}

+ 293 - 0
fs/gdrivefs/fileentry.go

@@ -0,0 +1,293 @@
+package gdrivefs
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	_ "log"
+	"net/http"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/jacobsa/fuse/fuseops"
+	drive "google.golang.org/api/drive/v3"
+)
+
+//FileEntry entry to handle files
+type FileEntry struct {
+	//parent *FileEntry
+	fs    *GDriveFS   // GDrive FS
+	GFile *drive.File // GDrive file
+	isDir bool        // Is dir
+	Name  string      // local name
+	// fuseops
+	Inode fuseops.InodeID
+	Attr  fuseops.InodeAttributes // Cached attributes
+
+	// cache file
+	tempFile *os.File // Cached file
+	// childs
+	children []*FileEntry // children
+}
+
+func NewFileEntry(fs *GDriveFS) *FileEntry {
+	return &FileEntry{
+		fs:       fs,
+		children: []*FileEntry{},
+		Attr:     fuseops.InodeAttributes{},
+	}
+}
+
+func (fe *FileEntry) AddChild(child *FileEntry) {
+	//child.parent = fe // is this needed at all?
+	// Solve name here?
+
+	fe.children = append(fe.children, child)
+}
+
+func (fe *FileEntry) RemoveChild(child *FileEntry) {
+	toremove := -1
+	for i, v := range fe.children {
+		if v == child {
+			toremove = i
+			break
+		}
+	}
+	if toremove == -1 {
+		return
+	}
+	fe.children = append(fe.children[:toremove], fe.children[toremove+1:]...)
+}
+
+// useful for debug to count children
+func (fe *FileEntry) Count() int {
+	count := 0
+
+	for _, c := range fe.children {
+		count += c.Count()
+	}
+	return count + len(fe.children)
+}
+
+// SetGFile update attributes and set drive.File
+func (fe *FileEntry) SetGFile(f *drive.File) {
+	// Create Attribute
+	attr := fuseops.InodeAttributes{}
+	attr.Nlink = 1
+	attr.Size = uint64(f.Size)
+	//attr.Size = uint64(f.QuotaBytesUsed)
+	// Temp
+	attr.Uid = fe.fs.getUID()
+	attr.Gid = fe.fs.getGID()
+	attr.Crtime, _ = time.Parse(time.RFC3339, f.CreatedTime)
+	attr.Ctime = attr.Crtime // Set CTime to created, although it is change inode metadata
+	attr.Mtime, _ = time.Parse(time.RFC3339, f.ModifiedTime)
+	attr.Atime = attr.Mtime // Set access time to modified, not sure if gdrive has access time
+
+	attr.Mode = os.FileMode(0644) // default
+
+	if f.MimeType == "application/vnd.google-apps.folder" {
+		attr.Mode = os.FileMode(0755) | os.ModeDir
+	}
+
+	fe.GFile = f
+	fe.Attr = attr
+}
+
+// Sync cached , upload to gdrive
+func (fe *FileEntry) Sync() (err error) {
+	if fe.tempFile == nil {
+		return
+	}
+
+	fe.tempFile.Sync()
+	fe.tempFile.Seek(0, io.SeekStart)
+
+	ngFile := &drive.File{}
+	up := fe.fs.srv.Files.Update(fe.GFile.Id, ngFile)
+	upFile, err := up.Media(fe.tempFile).Do()
+
+	fe.SetGFile(upFile) // update local GFile entry
+	return
+
+}
+
+//ClearCache remove local file
+func (fe *FileEntry) ClearCache() (err error) {
+	if fe.tempFile == nil {
+		return
+	}
+	fe.tempFile.Close()
+	os.Remove(fe.tempFile.Name())
+	fe.tempFile = nil
+	return
+}
+
+// Cache download GDrive file to a temporary local file or return already created file
+func (fe *FileEntry) Cache() *os.File {
+	if fe.tempFile != nil {
+		return fe.tempFile
+	}
+	var res *http.Response
+	var err error
+	// Export GDocs (Special google doc documents needs to be exported make a config somewhere for this)
+	switch fe.GFile.MimeType { // Make this somewhat optional
+	case "application/vnd.google-apps.document":
+		log.Println("Exporting as: text/markdown")
+		res, err = fe.fs.srv.Files.Export(fe.GFile.Id, "text/plain").Download()
+	case "application/vnd.google-apps.spreadsheet":
+		log.Println("Exporting as: text/csv")
+		res, err = fe.fs.srv.Files.Export(fe.GFile.Id, "text/csv").Download()
+	default:
+		res, err = fe.fs.srv.Files.Get(fe.GFile.Id).Download()
+	}
+
+	if err != nil {
+		log.Println("MimeType:", fe.GFile.MimeType)
+		log.Println("Error from GDrive API", err)
+		return nil
+	}
+	defer res.Body.Close()
+
+	// Local copy
+	fe.tempFile, err = ioutil.TempFile(os.TempDir(), "gdfs") // TODO: const this elsewhere
+	if err != nil {
+		log.Println("Error creating temp file")
+		return nil
+	}
+	io.Copy(fe.tempFile, res.Body)
+
+	fe.tempFile.Seek(0, io.SeekStart)
+	return fe.tempFile
+
+}
+
+// Find the right parent?
+// WRONG
+func (fe *FileEntry) solveAppendGFile(f *drive.File, inode fuseops.InodeID) *FileEntry {
+
+	fil := fe.FindByGID(f.Id, true)
+	if fil != nil { // ignore existing ID
+		return fil
+	}
+
+	if len(f.Parents) == 0 {
+		return fe.AppendGFile(f, inode) // = append(fs.root.fileList, entry)
+	}
+	for _, parent := range f.Parents { // hierarchy add
+		parentEntry := fe.FindByGID(parent, true)
+		if parentEntry == nil {
+			log.Fatalln("Non existent parent", parent)
+		}
+		// Here
+		return parentEntry.AppendGFile(f, inode)
+	}
+	return nil
+}
+
+// Load append whatever?
+// append file to this tree
+func (fe *FileEntry) AppendGFile(f *drive.File, inode fuseops.InodeID) *FileEntry {
+
+	name := f.Name
+	count := 1
+	nameParts := strings.Split(f.Name, ".")
+	for {
+		en := fe.FindByName(name, false) // locally only
+		if en == nil {                   // ok we want no value
+			break
+		}
+		count++
+		if len(nameParts) > 1 {
+			name = fmt.Sprintf("%s(%d).%s", nameParts[0], count, strings.Join(nameParts[1:], "."))
+		} else {
+			name = fmt.Sprintf("%s(%d)", nameParts[0], count)
+		}
+	}
+
+	// Create an entry
+
+	//log.Println("Creating new file entry for name:", name, "for GFile:", f.Name)
+	// lock from find inode to fileList append
+	entry := NewFileEntry(fe.fs)
+	entry.Name = name
+	entry.SetGFile(f)
+	entry.Inode = inode
+
+	fe.AddChild(entry)
+
+	//fe.fileList = append(fe.fileList, entry)
+	//fe.fileMap[f.Name] = entry
+
+	return entry
+}
+
+//FindByInode find by Inode or return self
+func (fe *FileEntry) FindByInode(inode fuseops.InodeID, recurse bool) *FileEntry {
+	if inode == fe.Inode {
+		return fe // return self
+	}
+	// Recurse??
+	for _, e := range fe.children {
+		if e.Inode == inode {
+			return e
+		}
+		if recurse {
+			re := e.FindByInode(inode, recurse)
+			if re != nil {
+				return re
+			}
+		}
+	}
+	// For each child we findByInode
+	return nil
+}
+
+// FindByName return a child entry by name
+func (fe *FileEntry) FindByName(name string, recurse bool) *FileEntry {
+	// Recurse??
+	for _, e := range fe.children {
+		if e.Name == name {
+			return e
+		}
+		if recurse {
+			re := e.FindByName(name, recurse)
+			if re != nil {
+				return re
+			}
+		}
+	}
+	// For each child we findByInode
+	return nil
+}
+
+// FindByGID find by google drive ID
+func (fe *FileEntry) FindByGID(gdriveID string, recurse bool) *FileEntry {
+	// Recurse??
+	for _, e := range fe.children {
+		if e.GFile.Id == gdriveID {
+			return e
+		}
+		if recurse {
+			re := e.FindByGID(gdriveID, recurse)
+			if re != nil {
+				return re
+			}
+		}
+	}
+	// For each child we findByInode
+	return nil
+}
+
+func (fe *FileEntry) FindUnusedInode() fuseops.InodeID {
+	var inode fuseops.InodeID
+	for inode = 2; inode < 99999; inode++ {
+		f := fe.FindByInode(inode, true)
+		if f == nil {
+			return inode
+		}
+	}
+	log.Println("0 Inode ODD")
+	return 0
+}

+ 655 - 0
fs/gdrivefs/gdrive-fuse.go

@@ -0,0 +1,655 @@
+// gdrivemount implements a google drive fuse driver
+package gdrivefs
+
+import (
+	"io"
+	"os"
+	"os/user"
+	"strconv"
+	"syscall"
+	"time"
+
+	"dev.hexasoftware.com/hxs/prettylog"
+
+	"golang.org/x/net/context"
+
+	drive "google.golang.org/api/drive/v3"
+	"google.golang.org/api/googleapi"
+
+	"github.com/jacobsa/fuse"
+	"github.com/jacobsa/fuse/fuseops"
+	"github.com/jacobsa/fuse/fuseutil"
+)
+
+var (
+	log = prettylog.New("gdrivemount")
+)
+
+type fileHandle struct {
+	handleID     fuseops.HandleID
+	entry        *FileEntry
+	uploadOnDone bool
+	// Testing
+	entries []fuseutil.Dirent
+	buf     []byte
+}
+
+/*type DirEntry struct {
+	file *FileEntry
+}*/
+
+// GDriveFS handler
+type GDriveFS struct {
+	fuseutil.NotImplementedFileSystem // Defaults
+	srv                               *drive.Service
+
+	osuser *user.User
+	root   *FileEntry // hiearchy reference
+
+	fileHandles map[fuseops.HandleID]*fileHandle
+	nextRefresh time.Time
+
+	//fileMap map[string]
+	// Map IDS with FileEntries
+}
+
+////////////////////////////////////////////////////////
+// TOOLS & HELPERS
+////////////////////////////////////////////////////////
+
+func (fs *GDriveFS) createHandle() *fileHandle {
+	// Lock here instead
+
+	var handle fuseops.HandleID
+
+	for handle = 1; handle < 99999; handle++ {
+		_, ok := fs.fileHandles[handle]
+		if !ok {
+			break
+		}
+	}
+
+	fh := &fileHandle{handleID: handle}
+	fs.fileHandles[handle] = fh
+
+	return fh
+}
+
+func NewGDriveFS() *GDriveFS {
+
+	osuser, err := user.Current()
+	if err != nil {
+		log.Fatalf("Unable to fetch current user:", err)
+	}
+
+	fs := &GDriveFS{}
+	fs.osuser = osuser
+	fs.srv = GetDriveService()
+	fs.root = &FileEntry{
+		fs: fs,
+		Attr: fuseops.InodeAttributes{
+			Mode:  os.FileMode(0755) | os.ModeDir,
+			Nlink: 1,
+			Size:  4096,
+			Uid:   fs.getUID(),
+			Gid:   fs.getGID(),
+		},
+		GFile: nil,
+		Inode: fuseops.RootInodeID,
+		Name:  "",
+		//fileMap: map[string]*FileEntry{},
+		children: []*FileEntry{},
+		isDir:    true,
+	}
+	fs.fileHandles = map[fuseops.HandleID]*fileHandle{}
+
+	// Temporary entry
+	entry := fs.root.AppendGFile(&drive.File{Id: "0", Name: "Loading..."}, 999999)
+	entry.Attr.Mode = os.FileMode(0)
+
+	fs.timedRefresh()
+
+	return fs
+}
+
+// Cache somewhere?
+func (fs *GDriveFS) getUID() uint32 {
+	uid, _ := strconv.Atoi(fs.osuser.Uid)
+	return uint32(uid)
+}
+func (fs *GDriveFS) getGID() uint32 {
+	gid, _ := strconv.Atoi(fs.osuser.Gid)
+	return uint32(gid)
+}
+
+func (fs *GDriveFS) timedRefresh() {
+
+	go func() {
+		for {
+			if time.Now().After(fs.nextRefresh) {
+				fs.Refresh()
+			}
+			time.Sleep(2 * time.Minute) // 2 minutes
+		}
+	}()
+
+}
+
+// Refresh service files
+func (fs *GDriveFS) Refresh() {
+	fs.nextRefresh = time.Now().Add(1 * time.Minute)
+
+	fileList := []*drive.File{}
+	fileMap := map[string]*drive.File{} // Temporary map by google drive fileID
+
+	gdFields := googleapi.Field("nextPageToken, files(id,name,size,quotaBytesUsed, mimeType,parents,createdTime,modifiedTime)")
+	log.Println("Loading file entries from gdrive")
+	r, err := fs.srv.Files.List().
+		OrderBy("createdTime").
+		PageSize(1000).
+		SupportsTeamDrives(true).
+		IncludeTeamDriveItems(true).
+		Fields(gdFields).
+		Do()
+	if err != nil {
+		log.Println("GDrive ERR:", err)
+		return
+	}
+	fileList = append(fileList, r.Files...)
+
+	// Rest of the pages
+	for r.NextPageToken != "" {
+		r, err = fs.srv.Files.List().
+			OrderBy("createdTime").
+			PageToken(r.NextPageToken).
+			Fields(gdFields).
+			Do()
+		if err != nil {
+			log.Println("GDrive ERR:", err)
+			return
+		}
+		fileList = append(fileList, r.Files...)
+	}
+	log.Println("Total entries:", len(fileList))
+
+	// TimeSort
+	/*log.Println("Sort by time")
+	sort.Slice(fileList, func(i, j int) bool {
+		createdTimeI, _ := time.Parse(time.RFC3339, fileList[i].CreatedTime)
+		createdTimeJ, _ := time.Parse(time.RFC3339, fileList[i].CreatedTime)
+		if createdTimeI.Before(createdTimeJ) {
+			return true
+		}
+		return false
+	})*/
+
+	// Cache ID for faster retrieval, might not be necessary
+	for _, f := range fileList {
+		fileMap[f.Id] = f
+	}
+
+	if err != nil || r == nil {
+		log.Println("Unable to retrieve files", err)
+		return
+	}
+
+	// Create clean fileList
+	root := NewFileEntry(fs)
+
+	// Helper func to recurse
+	// Everything loaded we add to our entries
+	// Add file and its parents priorizing it parent
+	var appendFile func(df *drive.File)
+	appendFile = func(df *drive.File) {
+		for _, pID := range df.Parents {
+			parentFile, ok := fileMap[pID]
+			if !ok {
+				parentFile, err = fs.srv.Files.Get(pID).Do()
+				if err != nil {
+					panic(err)
+				}
+				fileMap[parentFile.Id] = parentFile
+			}
+			appendFile(parentFile) // Recurse
+		}
+		// Find existing entry
+		var inode fuseops.InodeID
+		entry := fs.root.FindByGID(df.Id, true)
+		if entry == nil {
+			inode = root.FindUnusedInode()
+		} else {
+			inode = entry.Inode
+		}
+
+		newEntry := root.solveAppendGFile(df, inode)     // Find right parent
+		if entry != nil && entry.GFile.Name == df.Name { // Copy name from old entry
+			newEntry.Name = entry.Name
+		}
+
+		// add File
+	}
+
+	for _, f := range fileList { // Ordered
+		appendFile(f) // Check parent first
+	}
+
+	log.Println("Refresh done, update root")
+	fs.root.children = root.children
+
+	log.Println("File count:", fs.root.Count())
+}
+
+// OpenDir return nil error allows open dir
+func (fs *GDriveFS) OpenDir(ctx context.Context, op *fuseops.OpenDirOp) (err error) {
+
+	entry := fs.root.FindByInode(op.Inode, true)
+	if entry == nil {
+		return fuse.ENOENT
+	}
+
+	fh := fs.createHandle()
+	fh.entry = entry
+	op.Handle = fh.handleID
+
+	return // No error allow, dir open
+}
+
+// ReadDir lists files into readdirop
+func (fs *GDriveFS) ReadDir(ctx context.Context, op *fuseops.ReadDirOp) (err error) {
+	fh, ok := fs.fileHandles[op.Handle]
+	if !ok {
+		log.Fatal("Handle does not exists")
+	}
+
+	if op.Offset == 0 { // Rebuild/rewind dir list
+		fh.entries = []fuseutil.Dirent{}
+
+		for i, v := range fh.entry.children {
+			fusetype := fuseutil.DT_File
+			if v.isDir {
+				fusetype = fuseutil.DT_Directory
+			}
+			dirEnt := fuseutil.Dirent{
+				Inode:  v.Inode,
+				Name:   v.Name,
+				Type:   fusetype,
+				Offset: fuseops.DirOffset(i) + 1,
+			}
+			//	written += fuseutil.WriteDirent(fh.buf[written:], dirEnt)
+			fh.entries = append(fh.entries, dirEnt)
+		}
+	}
+
+	index := int(op.Offset)
+	if index > len(fh.entries) {
+		return fuse.EINVAL
+	}
+	if index > 0 {
+		index++
+	}
+	for i := index; i < len(fh.entries); i++ {
+		n := fuseutil.WriteDirent(op.Dst[op.BytesRead:], fh.entries[i])
+		//log.Println("Written:", n)
+		if n == 0 {
+			break
+		}
+		op.BytesRead += n
+	}
+	return
+}
+
+// SetInodeAttributes Not sure what attributes gdrive support we just leave this blank for now
+func (fs *GDriveFS) SetInodeAttributes(ctx context.Context, op *fuseops.SetInodeAttributesOp) (err error) {
+
+	// Hack to truncate file?
+
+	if op.Size != nil {
+		f := fs.root.FindByInode(op.Inode, true)
+
+		if *op.Size != 0 { // We only allow truncate to 0
+			return fuse.ENOSYS
+		}
+		// Delete and create another on truncate 0
+		err = fs.srv.Files.Delete(f.GFile.Id).Do() // XXX: Careful on this
+		createdFile, err := fs.srv.Files.Create(&drive.File{Parents: f.GFile.Parents, Name: f.GFile.Name}).Do()
+		if err != nil {
+			return fuse.EINVAL
+		}
+		f.SetGFile(createdFile) // Set new file
+	}
+
+	return
+}
+
+//GetInodeAttributes return attributes
+func (fs *GDriveFS) GetInodeAttributes(ctx context.Context, op *fuseops.GetInodeAttributesOp) (err error) {
+
+	f := fs.root.FindByInode(op.Inode, true)
+	if f == nil {
+		return fuse.ENOENT
+	}
+	op.Attributes = f.Attr
+	op.AttributesExpiration = time.Now().Add(time.Minute)
+
+	return
+}
+
+// ReleaseDirHandle deletes file handle entry
+func (fs *GDriveFS) ReleaseDirHandle(ctx context.Context, op *fuseops.ReleaseDirHandleOp) (err error) {
+	delete(fs.fileHandles, op.Handle)
+	return
+}
+
+// LookUpInode based on Parent and Name we return a self cached inode
+func (fs *GDriveFS) LookUpInode(ctx context.Context, op *fuseops.LookUpInodeOp) (err error) {
+
+	parentFile := fs.root.FindByInode(op.Parent, true) // true means transverse all
+	if parentFile == nil {
+		return fuse.ENOENT
+	}
+
+	now := time.Now()
+	// Transverse only local
+	f := parentFile.FindByName(op.Name, false)
+	if f == nil {
+		return fuse.ENOENT
+	}
+
+	op.Entry = fuseops.ChildInodeEntry{
+		Attributes:           f.Attr,
+		Child:                f.Inode,
+		AttributesExpiration: now.Add(time.Second),
+		EntryExpiration:      now.Add(time.Second),
+	}
+	return
+}
+
+// StatFS basically allows StatFS to run
+/*func (fs *GDriveFS) StatFS(ctx context.Context, op *fuseops.StatFSOp) (err error) {
+	return
+}*/
+
+// ForgetInode allows to forgetInode
+func (fs *GDriveFS) ForgetInode(ctx context.Context, op *fuseops.ForgetInodeOp) (err error) {
+	return
+}
+
+// GetXAttr special attributes
+func (fs *GDriveFS) GetXAttr(ctx context.Context, op *fuseops.GetXattrOp) (err error) {
+	return
+}
+
+//////////////////////////////////////////////////////////////////////////
+// File OPS
+//////////////////////////////////////////////////////////////////////////
+
+// OpenFile creates a temporary handle to be handled on read or write
+func (fs *GDriveFS) OpenFile(ctx context.Context, op *fuseops.OpenFileOp) (err error) {
+	f := fs.root.FindByInode(op.Inode, true) // might not exists
+
+	// Generate new handle
+	fh := fs.createHandle()
+	fh.entry = f
+
+	op.Handle = fh.handleID
+	op.UseDirectIO = true
+
+	return
+}
+
+// ReadFile  if the first time we download the google drive file into a local temporary file
+func (fs *GDriveFS) ReadFile(ctx context.Context, op *fuseops.ReadFileOp) (err error) {
+	lf := fs.fileHandles[op.Handle]
+
+	localFile := lf.entry.Cache()
+	op.BytesRead, err = localFile.ReadAt(op.Dst, op.Offset)
+	if err == io.EOF { // fuse does not expect a EOF
+		err = nil
+	}
+
+	return
+}
+
+// CreateFile creates empty file in google Drive and returns its ID and attributes, only allows file creation on 'My Drive'
+func (fs *GDriveFS) CreateFile(ctx context.Context, op *fuseops.CreateFileOp) (err error) {
+
+	parentFile := fs.root.FindByInode(op.Parent, true)
+	if parentFile == nil {
+		return fuse.ENOENT
+	}
+	// Only write on child folders
+	if parentFile.Inode == fuseops.RootInodeID {
+		return syscall.EPERM
+	}
+
+	existsFile := parentFile.FindByName(op.Name, false)
+	if existsFile != nil {
+		return fuse.EEXIST
+	}
+
+	// Generate ID
+	//genId, err := fs.srv.Files.GenerateIds().Count(1).Do()
+	//id := genId.Ids[0]
+	parents := []string{parentFile.GFile.Id}
+	newFile := &drive.File{
+		Parents: parents,
+		Name:    op.Name,
+	}
+	createdFile, err := fs.srv.Files.Create(newFile).Do()
+	if err != nil {
+		err = fuse.EINVAL
+		return
+	}
+
+	entry := parentFile.AppendGFile(createdFile, fs.root.FindUnusedInode()) // Add new created file
+	if entry == nil {
+		err = fuse.EINVAL
+		return
+	}
+
+	localFile := entry.Cache()
+	if localFile == nil {
+		return fuse.EINVAL
+	}
+	// Associate a temp file to a new handle
+	// Local copy
+	// Lock
+	fh := fs.createHandle()
+	fh.entry = entry
+	fh.uploadOnDone = true
+	//
+	op.Handle = fh.handleID
+	op.Entry = fuseops.ChildInodeEntry{
+		Attributes:           entry.Attr,
+		Child:                entry.Inode,
+		AttributesExpiration: time.Now().Add(time.Minute),
+		EntryExpiration:      time.Now().Add(time.Minute),
+	}
+	op.Mode = entry.Attr.Mode
+
+	return
+}
+
+// WriteFile as ReadFile it creates a temporary file on first read
+// Maybe the ReadFile should be called here aswell to cache current contents since we are using writeAt
+func (fs *GDriveFS) WriteFile(ctx context.Context, op *fuseops.WriteFileOp) (err error) {
+	lf, ok := fs.fileHandles[op.Handle]
+	if !ok {
+		return fuse.EIO
+	}
+
+	localFile := lf.entry.Cache()
+	if localFile == nil {
+		return fuse.EINVAL
+	}
+
+	_, err = localFile.WriteAt(op.Data, op.Offset)
+	if err != nil {
+		err = fuse.EIO
+		return
+	}
+	lf.uploadOnDone = true
+
+	return
+}
+
+// FlushFile just returns no error, maybe upload should be handled here
+func (fs *GDriveFS) FlushFile(ctx context.Context, op *fuseops.FlushFileOp) (err error) {
+	lf, ok := fs.fileHandles[op.Handle]
+	if !ok {
+		return fuse.EIO
+	}
+	if lf.entry.tempFile == nil {
+		return
+	}
+	if lf.uploadOnDone { // or if content changed basically
+		err = lf.entry.Sync()
+		if err != nil {
+			return fuse.EINVAL
+		}
+	}
+
+	return
+}
+
+// ReleaseFileHandle closes and deletes any temporary files, upload in case if changed locally
+func (fs *GDriveFS) ReleaseFileHandle(ctx context.Context, op *fuseops.ReleaseFileHandleOp) (err error) {
+	lf := fs.fileHandles[op.Handle]
+
+	/*if lf.uploadOnDone {
+		err = lf.entry.Sync()
+		if err != nil {
+			return fuse.EINVAL
+		}
+	}*/
+	lf.entry.ClearCache()
+	delete(fs.fileHandles, op.Handle)
+
+	return
+}
+
+// Unlink remove file and remove from local cache entry
+func (fs *GDriveFS) Unlink(ctx context.Context, op *fuseops.UnlinkOp) (err error) {
+	parentEntry := fs.root.FindByInode(op.Parent, true)
+	if parentEntry == nil {
+		return fuse.ENOENT
+	}
+	if parentEntry.Inode == fuseops.RootInodeID {
+		return syscall.EPERM
+	}
+
+	fileEntry := parentEntry.FindByName(op.Name, false)
+	if fileEntry == nil {
+		return fuse.ENOATTR
+	}
+	err = fs.srv.Files.Delete(fileEntry.GFile.Id).Do()
+	if err != nil {
+		return fuse.EIO
+	}
+
+	parentEntry.RemoveChild(fileEntry)
+
+	return
+}
+
+// MkDir creates a directory on a parent dir
+func (fs *GDriveFS) MkDir(ctx context.Context, op *fuseops.MkDirOp) (err error) {
+
+	parentFile := fs.root.FindByInode(op.Parent, true)
+	if parentFile == nil {
+		return fuse.ENOENT
+	}
+	if parentFile.Inode == fuseops.RootInodeID {
+		return syscall.EPERM
+	}
+
+	// Should check existent first too
+	fi, err := fs.srv.Files.Create(&drive.File{
+		Parents:  []string{parentFile.GFile.Id},
+		MimeType: "application/vnd.google-apps.folder",
+		Name:     op.Name,
+	}).Do()
+	if err != nil {
+		return fuse.ENOATTR
+	}
+	entry := parentFile.AppendGFile(fi, fs.root.FindUnusedInode())
+	if entry == nil {
+		return fuse.EINVAL
+	}
+
+	op.Entry = fuseops.ChildInodeEntry{
+		Attributes:           entry.Attr,
+		Child:                entry.Inode,
+		AttributesExpiration: time.Now().Add(time.Minute),
+		EntryExpiration:      time.Now().Add(time.Microsecond),
+	}
+
+	return
+}
+
+// RmDir fuse implementation
+func (fs *GDriveFS) RmDir(ctx context.Context, op *fuseops.RmDirOp) (err error) {
+
+	parentFile := fs.root.FindByInode(op.Parent, true)
+	if parentFile == nil {
+		return fuse.ENOENT
+	}
+	if parentFile.Inode == fuseops.RootInodeID {
+		return syscall.EPERM
+	}
+
+	theFile := parentFile.FindByName(op.Name, false)
+
+	err = fs.srv.Files.Delete(theFile.GFile.Id).Do()
+	if err != nil {
+		return fuse.ENOTEMPTY
+	}
+
+	parentFile.RemoveChild(theFile)
+
+	// Remove from entry somehow
+
+	return
+}
+
+// Rename fuse implementation
+func (fs *GDriveFS) Rename(ctx context.Context, op *fuseops.RenameOp) (err error) {
+	oldParentFile := fs.root.FindByInode(op.OldParent, true)
+	if oldParentFile == nil {
+		return fuse.ENOENT
+	}
+	newParentFile := fs.root.FindByInode(op.NewParent, true)
+	if newParentFile == nil {
+		return fuse.ENOENT
+	}
+
+	if oldParentFile.Inode == fuseops.RootInodeID || newParentFile.Inode == fuseops.RootInodeID {
+		return syscall.EPERM
+	}
+
+	oldFile := oldParentFile.FindByName(op.OldName, false)
+
+	// Although GDrive allows duplicate names, there is some issue with inode caching
+	// So we prevent a rename to a file with same name
+	existsFile := newParentFile.FindByName(op.NewName, false)
+	if existsFile != nil {
+		return fuse.EEXIST
+	}
+
+	ngFile := &drive.File{
+		Name: op.NewName,
+	}
+
+	updateCall := fs.srv.Files.Update(oldFile.GFile.Id, ngFile)
+	if oldParentFile != newParentFile {
+		updateCall.RemoveParents(oldParentFile.GFile.Id)
+		updateCall.AddParents(newParentFile.GFile.Id)
+	}
+	updatedFile, err := updateCall.Do()
+
+	oldParentFile.RemoveChild(oldFile)
+	newParentFile.AppendGFile(updatedFile, oldFile.Inode)
+
+	return
+
+}

+ 10 - 0
fs/gdrivefs/service.go

@@ -0,0 +1,10 @@
+package gdrivefs
+
+import "dev.hexasoftware.com/hxs/core"
+
+type Service interface {
+	core.Service
+}
+
+type gdriveService struct {
+}

+ 132 - 0
main.go

@@ -0,0 +1,132 @@
+//+build linux
+
+package main
+
+//go:generate go run cmd/genversion/main.go -package main -out version.go
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"os"
+
+	"os/exec"
+	"os/signal"
+	"runtime"
+	"syscall"
+
+	"dev.hexasoftware.com/hxs/cloudmount/fs/gdrivefs"
+	"github.com/jacobsa/fuse"
+	"github.com/jacobsa/fuse/fuseutil"
+
+	"dev.hexasoftware.com/hxs/prettylog"
+	//_ "github.com/icattlecoder/godaemon" // No reason
+)
+
+var (
+	log = prettylog.New("main")
+)
+
+func main() {
+	var daemonize bool
+	var verboselog bool
+	var clouddrive string
+
+	prettylog.Global()
+	// getClient
+	fmt.Printf("gdrivemount-%s\n\n", Version)
+
+	flag.StringVar(&clouddrive, "t", "gdrive", "which cloud service to use [gdrive]")
+	flag.BoolVar(&daemonize, "d", false, "Run app in background")
+	flag.BoolVar(&verboselog, "v", false, "Verbose log")
+
+	flag.Usage = func() {
+		fmt.Fprintf(os.Stderr, "Usage: %s [options] MOUNTPOINT\n\n", os.Args[0])
+		fmt.Fprintf(os.Stderr, "Options:\n")
+		flag.PrintDefaults()
+		fmt.Fprintf(os.Stderr, "\n")
+	}
+	flag.Parse()
+
+	if len(flag.Args()) < 1 {
+		flag.Usage()
+		//fmt.Println("Usage:\n gdrivemount [-d] [-v] MOUNTPOINT")
+		return
+	}
+
+	driveFS := gdrivefs.NewGDriveFS() // there can be some interaction before daemon
+
+	// Daemon
+	if daemonize {
+		subArgs := []string{}
+		for _, arg := range os.Args[1:] {
+			if arg == "-d" { // ignore daemon flag
+				continue
+			}
+			subArgs = append(subArgs, arg)
+		}
+
+		cmd := exec.Command(os.Args[0], subArgs...)
+		cmd.Start()
+		fmt.Println("[PID]", cmd.Process.Pid)
+		os.Exit(0)
+		return
+	}
+
+	//////////////
+	// Server
+	/////////
+	ctx := context.Background()
+	server := fuseutil.NewFileSystemServer(driveFS)
+	mountPath := flag.Arg(0)
+
+	var err error
+	var mfs *fuse.MountedFileSystem
+
+	if verboselog {
+		mfs, err = fuse.Mount(mountPath, server, &fuse.MountConfig{DebugLogger: prettylog.New("fuse"), ErrorLogger: prettylog.New("fuse-err")})
+	} else {
+		mfs, err = fuse.Mount(mountPath, server, &fuse.MountConfig{})
+	}
+	if err != nil {
+		log.Fatal("Failed mounting path", flag.Arg(0))
+	}
+
+	// Signal handling to refresh Drives
+	sigs := make(chan os.Signal, 2)
+	signal.Notify(sigs, syscall.SIGUSR1, syscall.SIGHUP, syscall.SIGINT, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		for sig := range sigs {
+			log.Println("Signal:", sig)
+			switch sig {
+			case syscall.SIGUSR1:
+				log.Println("Manually Refresh drive")
+				go driveFS.Refresh()
+			case syscall.SIGHUP:
+				log.Println("GC")
+				mem := runtime.MemStats{}
+				runtime.ReadMemStats(&mem)
+				log.Printf("Mem: %.2fMB", float64(mem.Alloc)/1024/1024)
+				runtime.GC()
+
+				runtime.ReadMemStats(&mem)
+				log.Printf("After gc: Mem: %.2fMB", float64(mem.Alloc)/1024/1024)
+
+			case os.Interrupt:
+				log.Println("Graceful unmount")
+				fuse.Unmount(mountPath)
+				os.Exit(1)
+			case syscall.SIGTERM:
+				log.Println("Graceful unmount")
+				fuse.Unmount(mountPath)
+				os.Exit(1)
+			}
+
+		}
+	}()
+
+	if err := mfs.Join(ctx); err != nil {
+		log.Fatalf("Joining: %v", err)
+	}
+
+}

+ 6 - 0
version.go

@@ -0,0 +1,6 @@
+package main
+
+const (
+  //Version contains version of the package
+  Version = "0.1 - built: 2017-07-07 10:30:51 UTC"
+)