Skip to content
This repository has been archived by the owner on Jun 14, 2018. It is now read-only.

Nextcloud file manager extension #30

Closed
viggy96 opened this issue Oct 4, 2016 · 17 comments
Closed

Nextcloud file manager extension #30

viggy96 opened this issue Oct 4, 2016 · 17 comments

Comments

@viggy96
Copy link

viggy96 commented Oct 4, 2016

Is there a plan to create a Nextcloud themed file manager extension, like owncloud-client-nautilus, or owncloud-client-nemo?

I really like being able to control link-sharing and such without using the web interface.

(particularly a Nemo extension)

@nickvergessen
Copy link
Member

Create the following file: /usr/share/nemo-python/extensions/syncstate.py

#
# Copyright (C) by Klaas Freitag <freitag@owncloud.com>
#
# This program is the core of OwnCloud integration to Nemo
# It will be installed on /usr/share/nemo-python/extensions/ with the paquet owncloud-client-nemo
# (https://github.com/owncloud/client/edit/master/shell_integration/nemo/syncstate.py)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.

import os
import urllib
import socket

from gi.repository import GObject, Nemo

# Please do not touch the following line.
# The reason is that we use a script to adopt this file for branding
# by replacing this line with the branding app name. If the following
# line is changed, the script can not match the pattern and fails.
appname = 'Nextcloud'

print("Initializing "+appname+"-client-nemo extension")


def get_local_path(url):
    if url[0:7] == 'file://':
        url = url[7:]
    return urllib.unquote(url)

def get_runtime_dir():
    """Returns the value of $XDG_RUNTIME_DIR, a directory path.

    If the value is not set, returns the same default as in Qt5
    """
    try:
        return os.environ['XDG_RUNTIME_DIR']
    except KeyError:
        fallback = '/tmp/runtime-' + os.environ['USER']
        return fallback


class SocketConnect(GObject.GObject):
    def __init__(self):
        GObject.GObject.__init__(self)
        self.connected = False
        self.registered_paths = {}
        self._watch_id = 0
        self._sock = None
        self._listeners = [self._update_registered_paths]
        self._remainder = ''
        self.nemoVFSFile_table = {}  # not needed in this object actually but shared 
                                         # all over the other objects.

        # returns true when one should try again!
        if self._connectToSocketServer():
            GObject.timeout_add(5000, self._connectToSocketServer)

    def reconnect(self):
        self._sock.close()
        self.connected = False
        GObject.source_remove(self._watch_id)
        GObject.timeout_add(5000, self._connectToSocketServer)

    def sendCommand(self, cmd):
        # print("Server command: " + cmd)
        if self.connected:
            try:
                self._sock.send(cmd)
            except:
                print("Sending failed.")
                self.reconnect()
        else:
            print("Cannot send, not connected!")

    def addListener(self, listener):
        self._listeners.append(listener)

    def _connectToSocketServer(self):
        try:
            self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            postfix = "/" + appname + "/socket"  # Should use os.path.join instead
            sock_file = get_runtime_dir() + postfix
            print ("Socket: " + sock_file + " <=> " + postfix)
            if sock_file != postfix:
                try:
                    print("Socket File: " + sock_file)
                    self._sock.connect(sock_file)
                    self.connected = True
                    print("Setting connected to %r." % self.connected )
                    self._watch_id = GObject.io_add_watch(self._sock, GObject.IO_IN, self._handle_notify)
                    print("Socket watch id: " + str(self._watch_id))
                    return False  # Don't run again
                except Exception as e:
                    print("Could not connect to unix socket. " + str(e))
            else:
                print("Sock-File not valid: " + sock_file)
        except Exception as e:  # Bad habbit
            print("Connect could not be established, try again later.")
            self._sock.close()

        return True  # Run again, if enabled via timeout_add()

    # Notify is the raw answer from the socket
    def _handle_notify(self, source, condition):
        data = source.recv(1024)
        # Prepend the remaining data from last call
        if len(self._remainder) > 0:
            data = self._remainder + data
            self._remainder = ''

        if len(data) > 0:
            # Remember the remainder for next round
            lastNL = data.rfind('\n');
            if lastNL > -1 and lastNL < len(data):
                self._remainder = data[lastNL+1:]
                data = data[:lastNL]

            for l in data.split('\n'):
                self._handle_server_response(l)
        else:
            return False

        return True  # Run again

    def _handle_server_response(self, line):
        print("Server response: " + line)
        parts = line.split(':')
        action = parts[0]
        args = parts[1:]

        for listener in self._listeners:
            listener(action, args)

    def _update_registered_paths(self, action, args):
        if action == 'REGISTER_PATH':
            self.registered_paths[args[0]] = 1
        elif action == 'UNREGISTER_PATH':
            del self.registered_paths[args[0]]

            # Check if there are no paths left. If so, its usual
            # that mirall went away. Try reconnecting.
            if not self.registered_paths:
                self.reconnect()

socketConnect = SocketConnect()


class MenuExtension(GObject.GObject, Nemo.MenuProvider):
    def __init__(self):
        GObject.GObject.__init__(self)

    def get_file_items(self, window, files):
        if len(files) != 1:
            return
        file = files[0]
        items = []

        # Internal or external file?!
        syncedFile = False
        for reg_path in socketConnect.registered_paths:
            topLevelFolder = False
            filename = get_local_path(file.get_uri())
            # Check if its a folder (ends with an /), if yes add a "/"
            # otherwise it will not find the entry in the table
            if os.path.isdir(filename + "/"):
                filename += "/"
                # Check if toplevel folder, we need to ignore those as they cannot be shared
                if filename == reg_path:
                    topLevelFolder=True                
            # Only show the menu extension if the file is synced and the sync
            # status is ok. Not for ignored files etc.
            # ignore top level folders
            if filename.startswith(reg_path) and topLevelFolder == False and socketConnect.nemoVFSFile_table[filename]['state'].startswith('OK'):
                syncedFile = True

        # If it is neither in a synced folder or is a directory
        if not syncedFile:
            return items

        # Create a menu item
        labelStr = "Share with " + appname + "..."
        item = Nemo.MenuItem(name='NemoPython::ShareItem', label=labelStr,
                tip='Share file {} through {}'.format(file.get_name(), appname) )
        item.connect("activate", self.menu_share, file)
        items.append(item)

        return items


    def menu_share(self, menu, file):
        filename = get_local_path(file.get_uri())
        print("Share file " + filename)
        socketConnect.sendCommand("SHARE:" + filename + "\n")


class SyncStateExtension(GObject.GObject, Nemo.ColumnProvider, Nemo.InfoProvider):
    def __init__(self):
        GObject.GObject.__init__(self)

        socketConnect.nemoVFSFile_table = {}
        socketConnect.addListener(self.handle_commands)

    def find_item_for_file(self, path):
        if path in socketConnect.nemoVFSFile_table:
            return socketConnect.nemoVFSFile_table[path]
        else:
            return None

    def askForOverlay(self, file):
        # print("Asking for overlay for "+file)  # For debug only
        if os.path.isdir(file):
            folderStatus = socketConnect.sendCommand("RETRIEVE_FOLDER_STATUS:"+file+"\n");

        if os.path.isfile(file):
            fileStatus = socketConnect.sendCommand("RETRIEVE_FILE_STATUS:"+file+"\n");

    def invalidate_items_underneath(self, path):
        update_items = []
        if not socketConnect.nemoVFSFile_table:
            self.askForOverlay(path)
        else:
            for p in socketConnect.nemoVFSFile_table:
                if p == path or p.startswith(path):
                    item = socketConnect.nemoVFSFile_table[p]['item']
                    update_items.append(p)

            for path1 in update_items:
                socketConnect.nemoVFSFile_table[path1]['item'].invalidate_extension_info()

    # Handles a single line of server response and sets the emblem
    def handle_commands(self, action, args):
        # file = args[0]  # For debug only
        # print("Action for " + file + ": " + args[0])  # For debug only
        if action == 'STATUS':
            newState = args[0]
            filename = ':'.join(args[1:])

            itemStore = self.find_item_for_file(filename)
            if itemStore:
                if( not itemStore['state'] or newState != itemStore['state'] ):
                    item = itemStore['item']

                    # print("Setting emblem on " + filename + "<>" + emblem + "<>")  # For debug only

                    # If an emblem is already set for this item, we need to
                    # clear the existing extension info before setting a new one.
                    #
                    # That will also trigger a new call to
                    # update_file_info for this item! That's why we set
                    # skipNextUpdate to True: we don't want to pull the
                    # current data from the client after getting a push
                    # notification.
                    invalidate = itemStore['state'] != None
                    if invalidate:
                        item.invalidate_extension_info()
                    self.set_emblem(item, newState)

                    socketConnect.nemoVFSFile_table[filename] = {
                        'item': item,
                        'state': newState,
                        'skipNextUpdate': invalidate }

        elif action == 'UPDATE_VIEW':
            # Search all items underneath this path and invalidate them
            if args[0] in socketConnect.registered_paths:
                self.invalidate_items_underneath(args[0])

        elif action == 'REGISTER_PATH':
            self.invalidate_items_underneath(args[0])
        elif action == 'UNREGISTER_PATH':
            self.invalidate_items_underneath(args[0])

    def set_emblem(self, item, state):
        Emblems = { 'OK'        : appname +'_ok',
                    'SYNC'      : appname +'_sync',
                    'NEW'       : appname +'_sync',
                    'IGNORE'    : appname +'_warn',
                    'ERROR'     : appname +'_error',
                    'OK+SWM'    : appname +'_ok_shared',
                    'SYNC+SWM'  : appname +'_sync_shared',
                    'NEW+SWM'   : appname +'_sync_shared',
                    'IGNORE+SWM': appname +'_warn_shared',
                    'ERROR+SWM' : appname +'_error_shared',
                    'NOP'       : ''
                  }

        emblem = 'NOP' # Show nothing if no emblem is defined.
        if state in Emblems:
            emblem = Emblems[state]
        item.add_emblem(emblem)

    def update_file_info(self, item):
        if item.get_uri_scheme() != 'file':
            return

        filename = get_local_path(item.get_uri())
        if item.is_directory():
            filename += '/'

        inScope = False
        for reg_path in socketConnect.registered_paths:
            if filename.startswith(reg_path):
                inScope = True
                break

        if not inScope:
            return

        # Ask for the current state from the client -- unless this update was
        # triggered by receiving a STATUS message from the client in the first
        # place.
        itemStore = self.find_item_for_file(filename)
        if itemStore and itemStore['skipNextUpdate'] and itemStore['state']:
            itemStore['skipNextUpdate'] = False
            itemStore['item'] = item
            self.set_emblem(item, itemStore['state'])
        else:
            socketConnect.nemoVFSFile_table[filename] = {
                'item': item,
                'state': None,
                'skipNextUpdate': False }

            # item.add_string_attribute('share_state', "share state")  # ?
            self.askForOverlay(filename)

@nickvergessen
Copy link
Member

PS: this is a copy of ownClouds syncstate.py file, just with an adjusted appname = 'Nextcloud'

@tvk7 tvk7 mentioned this issue Oct 27, 2016
@elpraga
Copy link

elpraga commented Jan 8, 2017

Would you know how to make the nautilus integraion work @nickvergessen ?

I tried your suggesion

PS: this is a copy of ownClouds syncstate.py file, just with an adjusted appname = 'Nextcloud'

in nautilus directory, but it didin't work.

@LostinSpacetime
Copy link

I tried to copy the syncstate.py file to /usr/share/nautilus-python/extensions without success.

@ckorn
Copy link

ckorn commented Mar 12, 2017

Please add an extension for Files (replacement of nautilus in later versions of gnome).

Is there at least the possibility to share a file via command line?

@enoch85
Copy link
Member

enoch85 commented Jun 5, 2017

This issue has been inactive for some time now.

Is this issue related to the ownCloud client, (thus not an theming isssue) please close it and report the issue here instead.
Is this issue related to this repo but solved, please close it and if possible let us know what solved the issue.
Is this issue still not solved, please let us know as well.

If I don't hear anything from the one who created this issue within 2 weeks, I will close the issue.

@nickvergessen
Copy link
Member

@enoch85 since it works for oc, but not for us, it has to be reported in our side?

@enoch85
Copy link
Member

enoch85 commented Jun 6, 2017

@nickvergessen I didn't read the issue itself, just posted above in all issues to close some that weren't active or invalid.

No one except me (?) is maintaining the issues here it seems. Would be great with some follow up one some issues, so that we could start closing stuff in this repo. :) Right now the pile is just getting bigger.

@enoch85
Copy link
Member

enoch85 commented Jun 6, 2017

Btw, Nextcloud is integrated in Gnome (Ubuntu Budgie 17.04). Works great! But maybe that's unrelated?

@C0rn3j
Copy link

C0rn3j commented Jul 29, 2017

@enoch85 Sadly it's not integrated in Files(nautilus) itself, so unrelated.

Would love to see an extension that'd work like the dropbox-nautilus one but for Nextcloud.

@elpraga
Copy link

elpraga commented Jul 29, 2017

@C0rn3j are you using the NextCloud client from PPA? For me it works just fine and I can use the Nautilus extension.

@C0rn3j
Copy link

C0rn3j commented Jul 29, 2017

@elpraga No, I am on Arch Linux, so I'm using the AUR package nextcloud-client (2.3.1). Could I get more information about the extension?

@nickvergessen
Copy link
Member

nickvergessen commented Jul 29, 2017

Arch packages are here: https://aur.archlinux.org/packages/nextcloud-client/

It even has nemo (linux mint file explorer) support, maybe you can contact them and ask for a version with the name nautilus (thats really the only thing you need to do: s/Nemo/Nautilus (in all kind of cases)

@C0rn3j
Copy link

C0rn3j commented Jul 29, 2017

@nickvergessen I contacted the maintainer and he seems as confused as me, could you clarify how exactly to get Nautilus integration working?

@juliushaertl
Copy link
Member

@C0rn3j The nextcloud-client aur package is already building with nautilus integration. It should install the syncstate script to: /usr/share/nautilus-python/extensions/syncstate-Nextcloud.py

My guess is that you need to install python2-nautilus as optional dependency in order to make it work.

@C0rn3j
Copy link

C0rn3j commented Jul 30, 2017

@juliushaertl Thank you, the AUR package is all good, I just totally missed that it has python2-nautilus as an optional dependency, silly me.

@juliushaertl
Copy link
Member

Ok, I guess we can close this then.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants