382 lines
12 KiB
Python
382 lines
12 KiB
Python
# Copyright (C) 2024 Lunatixz
|
|
#
|
|
#
|
|
# This file is part of PseudoTV Live.
|
|
#
|
|
# PseudoTV Live 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 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# PseudoTV Live 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.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with PseudoTV Live. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from globals import *
|
|
|
|
#constants
|
|
DEFAULT_ENCODING = "utf-8"
|
|
FILE_LOCK_MAX_FILE_TIMEOUT = 10
|
|
FILE_LOCK_NAME = "pseudotv"
|
|
|
|
class FileAccess:
|
|
@staticmethod
|
|
def open(filename, mode, encoding=DEFAULT_ENCODING):
|
|
fle = 0
|
|
try: return VFSFile(filename, mode)
|
|
except UnicodeDecodeError: return FileAccess.open(filename, mode, encoding)
|
|
return fle
|
|
|
|
|
|
@staticmethod
|
|
def _getFolderPath(path):
|
|
head, tail = os.path.split(path)
|
|
last_folder = os.path.basename(head)
|
|
return os.path.join(last_folder, tail)
|
|
|
|
|
|
@staticmethod
|
|
def listdir(path):
|
|
return xbmcvfs.listdir(path)
|
|
|
|
|
|
@staticmethod
|
|
def translatePath(path):
|
|
if '@' in path: path = path.split('@')[1]
|
|
return xbmcvfs.translatePath(path)
|
|
|
|
|
|
@staticmethod
|
|
def copyFolder(src, dir, dia=None, delete=False):
|
|
log('FileAccess: copyFolder %s to %s'%(src,dir))
|
|
if not FileAccess.exists(dir): FileAccess.makedirs(dir)
|
|
if dia:
|
|
from kodi import Dialog
|
|
DIALOG = Dialog()
|
|
|
|
subs, files = FileAccess.listdir(src)
|
|
pct = 0
|
|
if dia: dia = DIALOG.progressDialog(pct, control=dia, message='%s\n%s'%(LANGUAGE(32051),src))
|
|
for fidx, file in enumerate(files):
|
|
if dia: dia = DIALOG.progressDialog(pct, control=dia, message='%s: (%s%)\n%s'%(LANGUAGE(32051),(int(fidx*100)//len(files)),FileAccess._getFolderPath(file)))
|
|
if delete: FileAccess.move(os.path.join(src, file), os.path.join(dir, file))
|
|
else: FileAccess.copy(os.path.join(src, file), os.path.join(dir, file))
|
|
|
|
for sidx, sub in enumerate(subs):
|
|
pct = int(sidx)//len(subs)
|
|
FileAccess.copyFolder(os.path.join(src, sub), os.path.join(dir, sub), dia, delete)
|
|
|
|
|
|
@staticmethod
|
|
def copy(orgfilename, newfilename):
|
|
log('FileAccess: copying %s to %s'%(orgfilename,newfilename))
|
|
dir, file = os.path.split(newfilename)
|
|
if not FileAccess.exists(dir): FileAccess.makedirs(dir)
|
|
return xbmcvfs.copy(orgfilename, newfilename)
|
|
|
|
|
|
@staticmethod
|
|
def move(orgfilename, newfilename):
|
|
log('FileAccess: moving %s to %s'%(orgfilename,newfilename))
|
|
if FileAccess.copy(orgfilename, newfilename):
|
|
return FileAccess.delete(orgfilename)
|
|
return False
|
|
|
|
|
|
@staticmethod
|
|
def delete(filename):
|
|
return xbmcvfs.delete(filename)
|
|
|
|
|
|
@staticmethod
|
|
def exists(filename):
|
|
if filename.startswith('stack://'):
|
|
try: filename = (filename.split('stack://')[1].split(' , '))[0]
|
|
except Exception as e: log('FileAccess: exists failed! %s'%(e), xbmc.LOGERROR)
|
|
try:
|
|
return xbmcvfs.exists(filename)
|
|
except UnicodeDecodeError:
|
|
return os.path.exists(xbmcvfs.translatePath(filename))
|
|
return False
|
|
|
|
|
|
@staticmethod
|
|
def openSMB(filename, mode, encoding=DEFAULT_ENCODING):
|
|
fle = 0
|
|
if os.name.lower() == 'nt':
|
|
newname = '\\\\' + filename[6:]
|
|
try: fle = codecs.open(newname, mode, encoding)
|
|
except: fle = 0
|
|
return fle
|
|
|
|
|
|
@staticmethod
|
|
def existsSMB(filename):
|
|
if os.name.lower() == 'nt':
|
|
filename = '\\\\' + filename[6:]
|
|
return FileAccess.exists(filename)
|
|
return False
|
|
|
|
|
|
@staticmethod
|
|
def rename(path, newpath):
|
|
log("FileAccess: rename %s to %s"%(path,newpath))
|
|
if not FileAccess.exists(path):
|
|
return False
|
|
|
|
try:
|
|
if xbmcvfs.rename(path, newpath):
|
|
return True
|
|
except Exception as e:
|
|
log("FileAccess: rename, failed! %s"%(e), xbmc.LOGERROR)
|
|
|
|
try:
|
|
if FileAccess.move(path, newpath):
|
|
return True
|
|
except Exception as e:
|
|
log("FileAccess: move, failed! %s"%(e), xbmc.LOGERROR)
|
|
|
|
if path[0:6].lower() == 'smb://' or newpath[0:6].lower() == 'smb://':
|
|
if os.name.lower() == 'nt':
|
|
log("FileAccess: Modifying name")
|
|
if path[0:6].lower() == 'smb://':
|
|
path = '\\\\' + path[6:]
|
|
|
|
if newpath[0:6].lower() == 'smb://':
|
|
newpath = '\\\\' + newpath[6:]
|
|
|
|
if not os.path.exist(xbmcvfs.translatePath(path)):
|
|
return False
|
|
|
|
try:
|
|
log("FileAccess: os.rename")
|
|
os.rename(xbmcvfs.translatePath(path), xbmcvfs.translatePath(newpath))
|
|
return True
|
|
except Exception as e:
|
|
log("FileAccess: os.rename, failed! %s"%(e), xbmc.LOGERROR)
|
|
|
|
try:
|
|
log("FileAccess: shutil.move")
|
|
shutil.move(xbmcvfs.translatePath(path), xbmcvfs.translatePath(newpath))
|
|
return True
|
|
except Exception as e:
|
|
log("FileAccess: shutil.move, failed! %s"%(e), xbmc.LOGERROR)
|
|
|
|
log("FileAccess: OSError")
|
|
raise OSError()
|
|
|
|
|
|
@staticmethod
|
|
def removedirs(path, force=True):
|
|
if len(path) == 0: return False
|
|
elif(xbmcvfs.exists(path)):
|
|
return True
|
|
try:
|
|
success = xbmcvfs.rmdir(dir, force=force)
|
|
if success: return True
|
|
else: raise
|
|
except:
|
|
try:
|
|
os.rmdir(xbmcvfs.translatePath(path))
|
|
if os.path.exists(xbmcvfs.translatePath(path)):
|
|
return True
|
|
except: log("FileAccess: removedirs failed!", xbmc.LOGERROR)
|
|
return False
|
|
|
|
|
|
@staticmethod
|
|
def makedirs(directory):
|
|
try:
|
|
os.makedirs(xbmcvfs.translatePath(directory))
|
|
return os.path.exists(xbmcvfs.translatePath(directory))
|
|
except:
|
|
return FileAccess._makedirs(directory)
|
|
|
|
|
|
@staticmethod
|
|
def _makedirs(path):
|
|
if len(path) == 0:
|
|
return False
|
|
|
|
if(xbmcvfs.exists(path)):
|
|
return True
|
|
|
|
success = xbmcvfs.mkdir(path)
|
|
if success == False:
|
|
if path == os.path.dirname(xbmcvfs.translatePath(path)):
|
|
return False
|
|
|
|
if FileAccess._makedirs(os.path.dirname(xbmcvfs.translatePath(path))):
|
|
return xbmcvfs.mkdir(path)
|
|
return xbmcvfs.exists(path)
|
|
|
|
|
|
class VFSFile:
|
|
monitor = MONITOR()
|
|
|
|
def __init__(self, filename, mode):
|
|
if mode == 'w':
|
|
if not FileAccess.exists(filename):
|
|
FileAccess.makedirs(os.path.split(filename)[0])
|
|
self.currentFile = xbmcvfs.File(filename, 'wb')
|
|
else:
|
|
self.currentFile = xbmcvfs.File(filename, 'r')
|
|
log("VFSFile: Opening %s"%filename, xbmc.LOGDEBUG)
|
|
|
|
if self.currentFile == None:
|
|
log("VFSFile: Couldnt open %s"%filename, xbmc.LOGERROR)
|
|
|
|
|
|
def read(self, bytes=0):
|
|
try: return self.currentFile.read(bytes)
|
|
except: return self.currentFile.readBytes(bytes)
|
|
|
|
|
|
def readBytes(self, bytes=0):
|
|
return self.currentFile.readBytes(bytes)
|
|
|
|
|
|
def write(self, data):
|
|
if isinstance(data,bytes):
|
|
data = data.decode(DEFAULT_ENCODING, 'backslashreplace')
|
|
return self.currentFile.write(data)
|
|
|
|
|
|
def close(self):
|
|
return self.currentFile.close()
|
|
|
|
|
|
def seek(self, bytes, offset=1):
|
|
return self.currentFile.seek(bytes, offset)
|
|
|
|
|
|
def size(self):
|
|
return self.currentFile.size()
|
|
|
|
|
|
def readlines(self):
|
|
return self.read().split('\n')
|
|
# return list(self.readline())
|
|
|
|
|
|
def readline(self):
|
|
for line in self.read_in_chunks():
|
|
yield line
|
|
|
|
|
|
def tell(self):
|
|
try: return self.currentFile.tell()
|
|
except: return self.currentFile.seek(0, 1)
|
|
|
|
|
|
def read_in_chunks(self, chunk_size=1024):
|
|
"""Lazy function (generator) to read a file piece by piece."""
|
|
while not self.monitor.abortRequested():
|
|
data = self.read(chunk_size)
|
|
if not data: break
|
|
yield data
|
|
|
|
|
|
class FileLock(object):
|
|
monitor = MONITOR()
|
|
|
|
# https://github.com/dmfrey/FileLock
|
|
""" A file locking mechanism that has context-manager support so
|
|
you can use it in a with statement. This should be relatively cross
|
|
compatible as it doesn't rely on msvcrt or fcntl for the locking.
|
|
"""
|
|
|
|
def __init__(self, file_name=FILE_LOCK_NAME, timeout=FILE_LOCK_MAX_FILE_TIMEOUT, delay: float=0.5):
|
|
""" Prepare the file locker. Specify the file to lock and optionally
|
|
the maximum timeout and the delay between each attempt to lock.
|
|
"""
|
|
if timeout is not None and delay is None:
|
|
raise ValueError("If timeout is not None, then delay must not be None.")
|
|
|
|
self.is_locked = False
|
|
self.file_name = file_name
|
|
self.lockpath = self.checkpath()
|
|
self.lockfile = os.path.join(self.lockpath, "%s.lock" % self.file_name)
|
|
self.timeout = timeout
|
|
self.delay = delay
|
|
|
|
|
|
def checkpath(self):
|
|
lockpath = os.path.join(REAL_SETTINGS.getSetting('User_Folder'))
|
|
if not FileAccess.exists(lockpath):
|
|
if FileAccess.makedirs(lockpath):
|
|
return lockpath
|
|
else:#fallback to local folder.
|
|
#todo log error with lock path
|
|
lockpath = os.path.join(SETTINGS_LOC,'cache')
|
|
if not FileAccess.exists(lockpath):
|
|
FileAccess.makedirs(lockpath)
|
|
return lockpath
|
|
|
|
|
|
def acquire(self):
|
|
""" Acquire the lock, if possible. If the lock is in use, it check again
|
|
every `wait` seconds. It does this until it either gets the lock or
|
|
exceeds `timeout` number of seconds, in which case it throws
|
|
an exception.
|
|
"""
|
|
start_time = time.time()
|
|
while not self.monitor.abortRequested():
|
|
if self.monitor.waitForAbort(self.delay): break
|
|
else:
|
|
try:
|
|
self.fd = FileAccess.open(self.lockfile, 'w')
|
|
self.is_locked = True #moved to ensure tag only when locked
|
|
break;
|
|
except OSError as e:
|
|
if e.errno != errno.EEXIST:
|
|
raise
|
|
if self.timeout is None:
|
|
raise FileLockException("Could not acquire lock on {}".format(self.file_name))
|
|
if (time.time() - start_time) >= self.timeout:
|
|
raise FileLockException("Timeout occured.")
|
|
|
|
|
|
def release(self):
|
|
""" Get rid of the lock by deleting the lockfile.
|
|
When working in a `with` statement, this gets automatically
|
|
called at the end.
|
|
"""
|
|
if self.is_locked:
|
|
self.fd.close()
|
|
self.is_locked = False
|
|
|
|
|
|
def __enter__(self):
|
|
""" Activated when used in the with statement.
|
|
Should automatically acquire a lock to be used in the with block.
|
|
"""
|
|
if not self.is_locked:
|
|
self.acquire()
|
|
return self
|
|
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
""" Activated at the end of the with statement.
|
|
It automatically releases the lock if it isn't locked.
|
|
"""
|
|
if self.is_locked:
|
|
self.release()
|
|
|
|
|
|
def __del__(self):
|
|
""" Make sure that the FileLock instance doesn't leave a lockfile
|
|
lying around.
|
|
"""
|
|
self.release()
|
|
FileAccess.delete(self.lockfile)
|
|
|
|
|
|
class FileLockException(Exception):
|
|
pass |