Forum Archive

NAS Access

unad13

I have a collection of mp3 files on a NAS on my local network. I'd like to read the mp3 tags of some of these files using Pythonista. Can I access a file using its UNC path (e.g. "\NAS\Media\Music\Artist\Album\Song.mp3")? When I try to open that path, I get a "No such file or directory" exception.

It's a huge library and I don't want to put it in Dropbox.

dgelessus

Non-Windows systems generally don't support UNC paths (or any sort of path with backslashes as separators). On macOS you can use a smb: URL instead, in your case that would be smb://NAS/Media/Music/Artist/Album/Song.mp3. However iOS doesn't support network shares natively like macOS does, so opening a file from a SMB share isn't as straightforward as on macOS.

You could search on PyPI and see if you can find a library for accessing SMB shares. If it's written in pure Python, you can probably install it in Pythonista using Stash's pip command.

cvp

Personally, I use FTP to access my NAS

brumm

SMBclient

cvp

@brumm Hello, I want to use your smbclient but I see it needs to run in Python 2.
Would it be possible to run it in Python 3?
Or more generally, can I run a Python 3 script and call functions of a Python 2 imported module?

Edit: I see that two days ago, the needed impacket does not yet support Python 3

brumm

@cvp: My first library was pysmb, but then I switched to impacket, because of this issue. However it is fixed and pysmb is Python 3 ready. Of course you have to change the smb calls... Do you like to try it?

cvp

@brumm Yes, I would try in a few days. Thanks

cvp

@brumm do yo want to say I could use this

And is pysmb from here

brumm

@cvp oh yes, this is my first try. I think it should be a good start. Let me know when I can support you. And I saw there's another dependency - pyasn1, but it is also python3 ready.

cvp

@brumm Thanks for your future help, I'm sure I'll need it but not before some days.

cvp

@brumm I want to thank you one more time. I'm more than happy with your marvelous little test script. I have
- installed your smb-test.py
- installed python 3 version of smb and nmb
- from https://github.com/miketeo/pysmb/tree/master/python3
- installed pyasn1 (supporting python 3)
- from https://github.com/etingof/pyasn1
- tested getRemoteDir => ok
- tested download => errors
- modified to use BytesIO instead of StringIo
- to open local file as 'wb'
- tested upload => ok
- after same modif (open local file as 'rb')
- tested delete_remote_file => ok
- tested getServiceName => ok
- tested getBIOSName => ok (ip -> name)
- tested createRemoteDir => ok

- tested removeRemoteDir => ok (after correction, see here-under)

  • original code of smb-test.py did contain two removeRemoteDir
  • second one should be renameRemoteFileOrDir
  • tested renameRemoteFileOrDir => ok (for a dir)
  • tested renameRemoteFileOrDir => ok (for a file)
  • as my ip is dynamic, I created a new def getIP from fixed remote name
  • tested getIP => ok (name -> ip)

This has been a lot easier than I thought...
Now, I'll integrate that in my script, without connecting and closing the SMBconnection at each access.

def getIP(remote_name, timeout=5):
  try:
    bios = NetBIOS()
    ip = bios.queryName(remote_name)
  except Exception as e:
    print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)
  finally:
    bios.close()
    return ip[0]
cvp

And I put this in site-packages

# -*- coding: utf-8 -*-
# based   on   https://github.com/humberry/smb-example/blob/master/smb-test.py
# smb+nmb from https://github.com/miketeo/pysmb/tree/master/python3
# pyasn1  from https://github.com/etingof/pyasn1
from io import BytesIO
from smb.SMBConnection import SMBConnection
from smb import smb_structs
from nmb.NetBIOS import NetBIOS
import os
import sys
from socket import gethostname

class SMB_client():
    def __init__(self,username=None,password=None,smb_name=None):
        self.username     = username
        self.password     = password
        self.smb_name     = smb_name
        self.smb_ip       = None
        self.conn         = None
        self.service_name = None
        self.my_name      = None
        self.tree         = []

    def getBIOSName(self, remote_smb_ip, timeout=5):            # unused if dynamic IP
        # ip -> smb name
        try:
            bios = NetBIOS()
            srv_name = bios.queryIPForName(remote_smb_ip, timeout=timeout)
            return srv_name[0]
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)
            return None

    def getIP(self):
        # smb name -> ip
        try:
            bios = NetBIOS()
            ip = bios.queryName(self.smb_name)
            return ip[0]
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)
            return None

    def connect(self):
        try:
            self.my_name = gethostname()                # iDevice name
            self.smb_ip = self.getIP()
            smb_structs.SUPPORT_SMB2 = True
            self.conn = SMBConnection(self.username, self.password, self.my_name, self.smb_name, use_ntlm_v2 = True)
            self.conn.connect(self.smb_ip, 139)     #139=NetBIOS / 445=TCP
            if self.conn:
                shares = self.conn.listShares()
                for share in shares:
                    if share.type == 0:     # 0 = DISK_TREE
                        self.service_name = share.name  
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)

    def close(self):
        try:
            self.conn.close()
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)         

    def getRemoteDir(self, path, pattern):
        try:
            files = self.conn.listPath(self.service_name, path, pattern=pattern)
            return files
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)
            return None

    def getRemoteTree(self,path=''):
        try:
            if path == '':
                w = ''
            else:
                w = path+'/'
            files = self.getRemoteDir(path, '*')
            if files:
                for file in files:
                    if file.filename[0] == '.':
                        continue
                    self.tree.append({'name':w+file.filename, 'isdir':file.isDirectory, 'size':file.file_size})
                    if file.isDirectory:
                        self.getRemoteTree(path=w+file.filename)
            return self.tree
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)
            return None

    def download(self, path, filename):
        try:
            print('Download = ' + path + filename)
            attr = self.conn.getAttributes(self.service_name, path+filename)
            print('Size = %.1f kB' % (attr.file_size / 1024.0))
            print('start download')
            file_obj = BytesIO()
            file_attributes, filesize = self.conn.retrieveFile(self.service_name, path+filename, file_obj)
            fw = open(filename, 'wb')
            file_obj.seek(0)
            for line in file_obj:
                fw.write(line)
            fw.close()
            print('download finished')
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)

    def upload(self, path, filename):
        try:
            print('Upload = ' + path + filename)
            print('Size = %.1f kB' % (os.path.getsize(filename) / 1024.0))
            print('start upload')
            with open(filename, 'rb') as file_obj:
                filesize = self.conn.storeFile(self.service_name, path+filename, file_obj)
            print('upload finished')
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)

    def delete_remote_file(self,path, filename):
        try:
            self.conn.deleteFiles(self.service_name, path+filename)
            print('Remotefile ' + path + filename + ' deleted')
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)

    def createRemoteDir(self, path):
        try:
            self.conn.createDirectory(self.service_name, path)
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)

    def removeRemoteDir(self,path):
        try:
            self.conn.deleteDirectory(self.service_name, path)
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)

    def renameRemoteFileOrDir(self,old_path, new_path):
        try:
            self.conn.rename(self.service_name, old_path, new_path)
        except Exception as e:
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)

Use like

from SMB_client import SMB_client

my_smb = SMB_client(username='xxxxx',password='yuyy',smb_name='zzzzzz')
my_smb.connect()

tree = my_smb.getRemoteTree()
for elem in tree:
    print(elem)

#my_smb.download(path, filename)
#my_smb.upload(path, filename)
#my_smb.delete_remote_file(path, filename)
#my_smb.createRemoteDir(path)
#my_smb.removeRemoteDir(path)
#my_smb.renameRemoteFileOrDir(path, new_path)

my_smb.close()
brumm

I'm very happy that the code was useable.

cvp

@brumm Without your code, I never could get this smb working. Sincerely, thanks a lot

cvp

@brumm If you want to add some callback to upload and download...
```
def download(self, path, filename,buffersize=None,callback=None):
try:
#print('Download = ' + path + filename)
attr = self.conn.getAttributes(self.service_name, path+filename)
#print('Size = %.1f kB' % (attr.file_size / 1024.0))
#print('start download')
file_obj = BytesIO()
fw = open(filename, 'wb')
offset = 0
transmit =0
while True:
if not buffersize:
file_attributes, filesize = self.conn.retrieveFile(self.service_name, path+filename, file_obj)
else:
file_attributes, filesize = self.conn.retrieveFileFromOffset(self.service_name, path+filename, file_obj,offset=offset,max_length=buffersize)
if callback:
transmit = transmit + filesize
callback(transmit)
file_obj.seek(offset)
for line in file_obj:
fw.write(line)
offset = offset + filesize
if (not buffersize) or (filesize == 0):
break
fw.close()
#print('download finished')
except Exception as e:
print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).name, e)

def upload(self, path, filename,buffersize=None,callback=None):
    try:
        #print('Upload = ' + path + filename)
        #print('Size = %.1f kB' % (os.path.getsize(filename) / 1024.0))
        #print('start upload')
        file_obj = open(filename, 'rb')
        offset = 0
        while True:
            if not buffersize:
                filesize = self.conn.storeFile(self.service_name, path+filename, file_obj)
                break
            else:   
                buffer_obj = file_obj.read(buffersize)          
                if buffer_obj:
                    buffer_fileobj = BytesIO()
                    buffer_fileobj.write(buffer_obj)
                    buffer_fileobj.seek(0)
                    offset_new = self.conn.storeFileFromOffset(self.service_name, path+filename, buffer_fileobj, offset=offset, truncate=False)
                    #return the file position where the next byte will be written.
                    offset = offset_new
                    if callback:
                        callback(offset)
                else:
                    break
        file_obj.close()
        #print('upload finished')
    except Exception as e:
        print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e)```
brumm

@cvp Please feel free to upload it to your own github site, if you like. It's okay for me.

cvp

@brumm ok, Thanks, I'll do it

Edit: done at https://github.com/cvpe/Pythonista-scripts

cvp

@brumm As you have deeply studied SMB, I have a little question .
I use it to access (read and write) an USB key connected to an USB slot of my internet router.
Do you think I can remove safely the USB key as soon I have closed the smb connection?

brumm

@cvp I guess that your internet router has no (or a small) read/write buffer. Before you lose data I would recommend to use a USB key with a LED, to get sure that your router has enough time to write all data.

cvp

@brumm Thanks for the advice. But do you think that all buffers are flushed when the smb connection is closed?

brumm

@cvp When you use smb.SMBConnection it is a synchronous connection and you can send only one command after another. So it shouldn't be necessary to flush the buffer, but there could be a small gap until the cache of the router is empty. However I'm not an expert, so I would still recommend to use an USB key with a LED.

cvp

@brumm ok Thanks

elkrause

@cvp Sorry for my newbie question : Besides your SMB_Client.py you use more Python libs. How do I get these into Pythonista? Merci!

cvp

@elkrause if you check some posts above in this topic, I explain which libs I had installed, like nmb...

mikael

@elkrause, or maybe you want to know how to load packages from PyPi?

First install stash (google ”pythonista stash”), then it’s just ”pip install whatever” (as long as it’s pure python).

cvp

@mikael You're obviously right. I didn't have understood the request like that...

elkrause

@mikael @cvp Yes, this was my question. I have installed stash and are able to pip the standard libs. But I don‘t know how to download specific GitHib repos to be used as lib. Sorry for my newbie question.

cvp

@elkrause said:

how to download specific GitHib repos

There are several ways. Easiest, if available, is to download a folder as .zip via

File will arrive in Files from where you can copy/import to Pythonista and then unzip the file to your sites-package folder.

cvp

@elkrause better is to unzip in Files app, then in split view, drag and drop needed folder to Pythonista site-packages

elkrause

Thanks @cvp for explaining the basics! I managed to install the libs. I needed to be logged in GitHub to see the download link.