Browse Source

Py3: Move all filesystem operations to seperate file

pull/1161/head
Safihre 8 years ago
parent
commit
bac5523ce3
  1. 11
      SABnzbd.py
  2. 11
      sabnzbd/__init__.py
  3. 21
      sabnzbd/api.py
  4. 5
      sabnzbd/assembler.py
  5. 15
      sabnzbd/config.py
  6. 5
      sabnzbd/directunpacker.py
  7. 12
      sabnzbd/dirscanner.py
  8. 860
      sabnzbd/filesystem.py
  9. 7
      sabnzbd/getipaddress.py
  10. 6
      sabnzbd/interface.py
  11. 857
      sabnzbd/misc.py
  12. 7
      sabnzbd/newsunpack.py
  13. 3
      sabnzbd/newswrapper.py
  14. 7
      sabnzbd/notifier.py
  15. 4
      sabnzbd/nzbqueue.py
  16. 11
      sabnzbd/nzbstuff.py
  17. 2
      sabnzbd/osxmenu.py
  18. 10
      sabnzbd/postproc.py
  19. 2
      sabnzbd/tvsort.py
  20. 9
      sabnzbd/urlgrabber.py
  21. 8
      sabnzbd/utils/pathbrowser.py
  22. 2
      sabnzbd/utils/rarfile.py
  23. 2
      sabnzbd/utils/upload.py

11
SABnzbd.py

@ -68,10 +68,10 @@ import sabnzbd.lang
import sabnzbd.interface
from sabnzbd.constants import *
import sabnzbd.newsunpack
from sabnzbd.misc import real_path, \
check_latest_version, exit_sab, get_from_url, \
split_host, get_ext, create_https_certificates, \
windows_variant, ip_extract, set_serv_parms, get_serv_parms, globber_full
from sabnzbd.misc import check_latest_version, exit_sab, \
split_host, create_https_certificates, windows_variant, ip_extract, \
set_serv_parms, get_serv_parms
from sabnzbd.filesystem import get_ext, real_path, long_path, globber_full
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, \
panic_sqlite, panic, launch_a_browser
import sabnzbd.scheduler as scheduler
@ -940,6 +940,9 @@ def main():
# Find out where INI file is
inifile = os.path.abspath(sabnzbd.DIR_LCLDATA + '/' + DEF_INI_FILE)
# Long-path notation on Windows to be sure
inifile = long_path(inifile)
# If INI file at non-std location, then use INI location as $HOME
if sabnzbd.DIR_LCLDATA != os.path.dirname(inifile):
sabnzbd.DIR_HOME = os.path.dirname(inifile)

11
sabnzbd/__init__.py

@ -80,6 +80,7 @@ from sabnzbd.downloader import Downloader
from sabnzbd.assembler import Assembler
from sabnzbd.rating import Rating
import sabnzbd.misc as misc
import sabnzbd.filesystem as filesystem
import sabnzbd.powersup as powersup
from sabnzbd.dirscanner import DirScanner, ProcessArchiveFile, ProcessSingleFile
from sabnzbd.urlgrabber import URLGrabber
@ -224,7 +225,7 @@ def initialize(pause_downloader=False, clean_up=False, evalSched=False, repair=0
# Clean-up, if requested
if clean_up:
# New admin folder
misc.remove_all(cfg.admin_dir.get_path(), '*.sab')
filesystem.remove_all(cfg.admin_dir.get_path(), '*.sab')
# Optionally wait for "incomplete" to become online
if cfg.wait_for_dfolder():
@ -234,13 +235,13 @@ def initialize(pause_downloader=False, clean_up=False, evalSched=False, repair=0
cfg.download_dir.set_create(True)
# Set access rights for "incomplete" base folder
misc.set_permissions(cfg.download_dir.get_path(), recursive=False)
filesystem.set_permissions(cfg.download_dir.get_path(), recursive=False)
# If dirscan_dir cannot be created, set a proper value anyway.
# Maybe it's a network path that's temporarily missing.
path = cfg.dirscan_dir.get_path()
if not os.path.exists(path):
sabnzbd.misc.create_real_path(cfg.dirscan_dir.ident(), '', path, False)
filesystem.create_real_path(cfg.dirscan_dir.ident(), '', path, False)
# Set call backs for Config items
cfg.cache_limit.callback(new_limit)
@ -1093,8 +1094,8 @@ def pid_file(pid_path=None, pid_file=None, port=0):
def check_incomplete_vs_complete():
""" Make sure "incomplete" and "complete" are not identical """
complete = cfg.complete_dir.get_path()
if misc.same_file(cfg.download_dir.get_path(), complete):
if misc.real_path('X', cfg.download_dir()) == cfg.download_dir():
if filesystem.same_file(cfg.download_dir.get_path(), complete):
if filesystem.real_path('X', cfg.download_dir()) == cfg.download_dir():
# Abs path, so set an abs path too
cfg.download_dir.set(os.path.join(complete, 'incomplete'))
else:

21
sabnzbd/api.py

@ -48,9 +48,11 @@ from sabnzbd.skintext import SKIN_TEXT
from sabnzbd.utils.rsslib import RSS, Item
from sabnzbd.utils.pathbrowser import folders_at_path
from sabnzbd.utils.getperformance import getcpu
from sabnzbd.misc import loadavg, to_units, diskspace, get_ext, \
get_filename, int_conv, globber, globber_full, time_format, remove_all, \
starts_with_path, cat_convert, clip_path, create_https_certificates, calc_age
from sabnzbd.misc import loadavg, to_units, int_conv, time_format, \
cat_convert, create_https_certificates, calc_age
from sabnzbd.filesystem import diskspace, get_ext, get_filename, globber, \
globber_full, clip_path, remove_all
from sabnzbd.filesystem import same_file
from sabnzbd.encoding import xml_name, unicoder, special_fixer, platform_encode, html_escape
from sabnzbd.postproc import PostProcessor
from sabnzbd.articlecache import ArticleCache
@ -1232,7 +1234,6 @@ def build_status(skip_dashboard=False, output=None):
if server.request and not server.info:
connected = T(' Resolving address').replace(' ', '')
serverconnections.sort()
# For the templates or for JSON
if output:
@ -1684,7 +1685,7 @@ def build_queue_header(search=None, start=0, limit=0, output=None):
try:
datestart = datetime.datetime.now() + datetime.timedelta(seconds=bytesleft / bytespersec)
# new eta format: 16:00 Fri 07 Feb
header['eta'] = datestart.strftime(time_format('%H:%M %a %d %b')).decode(codepage)
header['eta'] = datestart.strftime(time_format('%H:%M %a %d %b'))
except:
datestart = datetime.datetime.now()
header['eta'] = T('unknown')
@ -1812,7 +1813,7 @@ def build_history(start=None, limit=None, verbose=False, verbose_list=None, sear
item['retry'] = int(bool(item.get('status') == 'Failed' and
path and
path not in retry_folders and
starts_with_path(path, cfg.download_dir.get_path()) and
same_file(path, cfg.download_dir.get_path()) and
os.path.exists(path)) and
not bool(globber(os.path.join(path, JOB_ADMIN), 'SABnzbd_n*'))
)
@ -1869,12 +1870,6 @@ def get_active_history(queue=None, items=None):
item['size'] = format_bytes(item['bytes'])
else:
item['size'] = ''
# Queue display needs Unicode instead of UTF-8
for kw in item:
if isinstance(item[kw], str):
item[kw] = item[kw].decode('utf-8')
items.append(item)
return items
@ -1913,7 +1908,7 @@ def calc_timeleft(bytesleft, bps):
def std_time(when):
# Fri, 16 Nov 2007 16:42:01 GMT +0100
item = time.strftime(time_format('%a, %d %b %Y %H:%M:%S'), time.localtime(when)).decode(codepage)
item = time.strftime(time_format('%a, %d %b %Y %H:%M:%S'), time.localtime(when))
item += " GMT %+05d" % (-time.timezone / 36)
return item

5
sabnzbd/assembler.py

@ -28,8 +28,9 @@ from time import sleep
import hashlib
import sabnzbd
from sabnzbd.misc import get_filepath, sanitize_filename, get_unique_filename, renamer, \
set_permissions, long_path, clip_path, has_win_device, get_all_passwords, diskspace, \
from sabnzbd.misc import get_all_passwords
from sabnzbd.filesystem import get_filepath, sanitize_filename, get_unique_filename, \
renamer, set_permissions, long_path, clip_path, has_win_device, diskspace, \
get_filename, get_ext
from sabnzbd.constants import Status, GIGI
import sabnzbd.cfg as cfg

15
sabnzbd/config.py

@ -29,6 +29,7 @@ import random
import uuid
from urllib.parse import urlparse
import sabnzbd.misc
import sabnzbd.filesystem
from sabnzbd.constants import CONFIG_VERSION, NORMAL_PRIORITY, DEFAULT_PRIORITY, MAX_WIN_DFOLDER
import configobj
from sabnzbd.decorators import synchronized
@ -206,16 +207,16 @@ class OptionDir(Option):
value = self.get()
path = ''
if value:
path = sabnzbd.misc.real_path(self.__root, value)
path = sabnzbd.filesystem.real_path(self.__root, value)
if self.__create and not os.path.exists(path):
res, path = sabnzbd.misc.create_real_path(self.ident()[1], self.__root, value, self.__apply_umask, self.__writable)
res, path = sabnzbd.filesystem.create_real_path(self.ident()[1], self.__root, value, self.__apply_umask, self.__writable)
return path
def test_path(self):
""" Return True if path exists """
value = self.get()
if value:
return os.path.exists(sabnzbd.misc.real_path(self.__root, value))
return os.path.exists(sabnzbd.filesystem.real_path(self.__root, value))
else:
return False
@ -236,7 +237,7 @@ class OptionDir(Option):
error, value = self.__validation(self.__root, value, self._Option__default_val)
if not error:
if value and (self.__create or create):
res, path = sabnzbd.misc.create_real_path(self.ident()[1], self.__root, value, self.__apply_umask, self.__writable)
res, path = sabnzbd.filesystem.create_real_path(self.ident()[1], self.__root, value, self.__apply_umask, self.__writable)
if not res:
error = T('Cannot create %s folder %s') % (self.ident()[1], path)
if not error:
@ -834,7 +835,7 @@ def save_config(force=False):
bakname = filename + '.bak'
# Check if file is writable
if not sabnzbd.misc.is_writable(filename):
if not sabnzbd.filesystem.is_writable(filename):
logging.error(T('Cannot write to INI file %s'), filename)
return res
@ -863,7 +864,7 @@ def save_config(force=False):
except:
pass
# Restore INI file from backup
sabnzbd.misc.renamer(bakname, filename)
sabnzbd.filesystem.renamer(bakname, filename)
return res
@ -1075,7 +1076,7 @@ def validate_safedir(root, value, default):
""" Allow only when queues are empty and no UNC
On Windows path should be small
"""
if sabnzbd.WIN32 and value and len(sabnzbd.misc.real_path(root, value)) >= MAX_WIN_DFOLDER:
if sabnzbd.WIN32 and value and len(sabnzbd.filesystem.real_path(root, value)) >= MAX_WIN_DFOLDER:
return T('Error: Path length should be below %s.') % MAX_WIN_DFOLDER, None
if sabnzbd.empty_queues():
return validate_no_unc(root, value, default)

5
sabnzbd/directunpacker.py

@ -29,8 +29,9 @@ from subprocess import Popen
import sabnzbd
import sabnzbd.cfg as cfg
from sabnzbd.misc import int_conv, clip_path, long_path, remove_all, globber, \
format_time_string, has_win_device, real_path, remove_file
from sabnzbd.misc import int_conv, format_time_string
from sabnzbd.filesystem import clip_path, long_path, remove_all, globber, \
has_win_device, real_path
from sabnzbd.encoding import TRANS, unicoder
from sabnzbd.newsunpack import build_command, EXTRACTFROM_RE, EXTRACTED_RE, rar_volumelist
from sabnzbd.postproc import prepare_extraction_path

12
sabnzbd/dirscanner.py

@ -34,7 +34,7 @@ from sabnzbd.decorators import NzbQueueLocker
from sabnzbd.encoding import ubtou, platform_encode
from sabnzbd.newsunpack import is_sevenfile, SevenZip
import sabnzbd.nzbstuff as nzbstuff
import sabnzbd.misc as misc
import sabnzbd.filesystem
import sabnzbd.config as config
import sabnzbd.cfg as cfg
@ -164,7 +164,7 @@ def ProcessArchiveFile(filename, path, pp=None, script=None, cat=None, catdir=No
if not keep:
misc.remove_file(path)
except:
logging.error(T('Error removing %s'), misc.clip_path(path))
logging.error(T('Error removing %s'), sabnzbd.filesystem.clip_path(path))
logging.info("Traceback: ", exc_info=True)
status = 1
else:
@ -208,7 +208,7 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
data = f.read()
f.close()
except:
logging.warning(T('Cannot read %s'), misc.clip_path(path))
logging.warning(T('Cannot read %s'), sabnzbd.filesystem.clip_path(path))
logging.info("Traceback: ", exc_info=True)
return -2, nzo_ids
@ -252,7 +252,7 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
if not keep:
misc.remove_file(path)
except:
logging.error(T('Error removing %s'), misc.clip_path(path))
logging.error(T('Error removing %s'), sabnzbd.filesystem.clip_path(path))
logging.info("Traceback: ", exc_info=True)
return 1, nzo_ids
@ -349,7 +349,7 @@ class DirScanner(threading.Thread):
files = os.listdir(folder)
except:
if not self.error_reported and not catdir:
logging.error(T('Cannot read Watched Folder %s'), misc.clip_path(folder))
logging.error(T('Cannot read Watched Folder %s'), sabnzbd.filesystem.clip_path(folder))
self.error_reported = True
files = []
@ -432,7 +432,7 @@ class DirScanner(threading.Thread):
list = os.listdir(dirscan_dir)
except:
if not self.error_reported:
logging.error(T('Cannot read Watched Folder %s'), misc.clip_path(dirscan_dir))
logging.error(T('Cannot read Watched Folder %s'), sabnzbd.filesystem.clip_path(dirscan_dir))
self.error_reported = True
list = []

860
sabnzbd/filesystem.py

@ -0,0 +1,860 @@
#!/usr/bin/python -OO
# Copyright 2008-2017 The SABnzbd-Team <team@sabnzbd.org>
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
sabnzbd.misc - filesystem operations
"""
import os
import sys
import logging
import urllib.request, urllib.parse, urllib.error
import re
import shutil
import threading
import subprocess
import socket
import time
import datetime
import fnmatch
import stat
from urllib.parse import urlparse
import sabnzbd
from sabnzbd.decorators import synchronized
from sabnzbd.constants import DEFAULT_PRIORITY, FUTURE_Q_FOLDER, JOB_ADMIN, \
GIGI, MEBI, DEF_CACHE_LIMIT
from sabnzbd.encoding import ubtou, unicoder, special_fixer, gUTF
def get_ext(filename):
""" Return lowercased file extension """
try:
return os.path.splitext(filename)[1].lower()
except:
return ''
def get_filename(path):
""" Return path without the file extension """
try:
return os.path.split(path)[1]
except:
return ''
def is_writable(path):
""" Return True is file is writable (also when non-existent) """
if os.path.isfile(path):
return bool(os.stat(path).st_mode & stat.S_IWUSR)
else:
return True
_DEVICES = ('con', 'prn', 'aux', 'nul',
'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9')
def replace_win_devices(name):
''' Remove reserved Windows device names from a name.
aux.txt ==> _aux.txt
txt.aux ==> txt.aux
'''
if name:
lname = name.lower()
for dev in _DEVICES:
if lname == dev or lname.startswith(dev + '.'):
name = '_' + name
break
# Remove special NTFS filename
if lname.startswith('$mft'):
name = name.replace('$', 'S', 1)
return name
def has_win_device(p):
""" Return True if filename part contains forbidden name
Before and after sanitizing
"""
p = os.path.split(p)[1].lower()
for dev in _DEVICES:
if p == dev or p.startswith(dev + '.') or p.startswith('_' + dev + '.'):
return True
return False
if sabnzbd.WIN32:
# the colon should be here too, but we'll handle that separately
CH_ILLEGAL = '\/<>?*|"\t'
CH_LEGAL = '++{}!@#`+'
else:
CH_ILLEGAL = '/'
CH_LEGAL = '+'
def sanitize_filename(name):
""" Return filename with illegal chars converted to legal ones
and with the par2 extension always in lowercase
"""
if not name:
return name
illegal = CH_ILLEGAL
legal = CH_LEGAL
if ':' in name:
if sabnzbd.WIN32:
# Compensate for the odd way par2 on Windows substitutes a colon character
name = name.replace(':', '3A')
elif sabnzbd.DARWIN:
# Compensate for the foolish way par2 on OSX handles a colon character
name = name[name.rfind(':') + 1:]
if sabnzbd.WIN32 or sabnzbd.cfg.sanitize_safe():
name = replace_win_devices(name)
lst = []
for ch in name.strip():
if ch in illegal:
ch = legal[illegal.find(ch)]
lst.append(ch)
name = ''.join(lst)
if not name:
name = 'unknown'
name, ext = os.path.splitext(name)
lowext = ext.lower()
if lowext == '.par2' and lowext != ext:
ext = lowext
return name + ext
def sanitize_foldername(name, limit=True):
""" Return foldername with dodgy chars converted to safe ones
Remove any leading and trailing dot and space characters
"""
if not name:
return name
illegal = CH_ILLEGAL + ':\x92"'
legal = CH_LEGAL + "-''"
if sabnzbd.cfg.sanitize_safe():
# Remove all bad Windows chars too
illegal += r'\/<>?*|":'
legal += r'++{}!@#`;'
repl = sabnzbd.cfg.replace_illegal()
lst = []
for ch in name.strip():
if ch in illegal:
if repl:
ch = legal[illegal.find(ch)]
lst.append(ch)
else:
lst.append(ch)
name = ''.join(lst)
name = name.strip()
if sabnzbd.WIN32 or sabnzbd.cfg.sanitize_safe():
name = replace_win_devices(name)
maxlen = sabnzbd.cfg.folder_max_length()
if limit and len(name) > maxlen:
name = name[:maxlen]
# And finally, make sure it doesn't end in a dot
if name != '.' and name != '..':
name = name.rstrip('.')
if not name:
name = 'unknown'
return name
def sanitize_and_trim_path(path):
""" Remove illegal characters and trim element size """
path = path.strip()
new_path = ''
if sabnzbd.WIN32:
if path.startswith('\\\\?\\UNC\\'):
new_path = '\\\\?\\UNC\\'
path = path[8:]
elif path.startswith('\\\\?\\'):
new_path = '\\\\?\\'
path = path[4:]
path = path.replace('\\', '/')
parts = path.split('/')
if sabnzbd.WIN32 and len(parts[0]) == 2 and ':' in parts[0]:
new_path += parts[0] + '/'
parts.pop(0)
elif path.startswith('//'):
new_path = '//'
elif path.startswith('/'):
new_path = '/'
for part in parts:
new_path = os.path.join(new_path, sanitize_foldername(part))
return os.path.abspath(os.path.normpath(new_path))
def sanitize_files_in_folder(folder):
""" Sanitize each file in the folder, return list of new names
"""
lst = []
for root, _, files in os.walk(folder):
for file_ in files:
path = os.path.join(root, file_)
new_path = os.path.join(root, sanitize_filename(file_))
if path != new_path:
try:
os.rename(path, new_path)
path = new_path
except:
logging.debug('Cannot rename %s to %s', path, new_path)
lst.append(path)
return lst
def is_obfuscated_filename(filename):
""" Check if this file has an extension, if not, it's
probably obfuscated and we don't use it
"""
return (os.path.splitext(filename)[1] == '')
def create_all_dirs(path, umask=False):
""" Create all required path elements and set umask on all
Return True if last element could be made or exists
"""
result = True
if sabnzbd.WIN32:
try:
os.makedirs(path)
except:
result = False
else:
lst = []
lst.extend(path.split('/'))
path = ''
for d in lst:
if d:
path += '/' + d
if not os.path.exists(path):
try:
os.mkdir(path)
result = True
except:
result = False
if umask:
mask = sabnzbd.cfg.umask()
if mask:
try:
os.chmod(path, int(mask, 8) | 0o700)
except:
pass
return result
def real_path(loc, path):
""" When 'path' is relative, return normalized join of 'loc' and 'path'
When 'path' is absolute, return normalized path
A path starting with ~ will be located in the user's Home folder
"""
# The Windows part is a bit convoluted because
# os.path.join() doesn't behave the same for all Python versions
if path:
path = path.strip()
else:
path = ''
if path:
if not sabnzbd.WIN32 and path.startswith('~/'):
path = path.replace('~', os.environ.get('HOME', sabnzbd.DIR_HOME), 1)
if sabnzbd.WIN32:
path = path.replace('/', '\\')
if len(path) > 1 and path[0].isalpha() and path[1] == ':':
if len(path) == 2 or path[2] != '\\':
path = path.replace(':', ':\\', 1)
elif path.startswith('\\\\'):
pass
elif path.startswith('\\'):
if len(loc) > 1 and loc[0].isalpha() and loc[1] == ':':
path = loc[:2] + path
else:
path = os.path.join(loc, path)
elif path[0] != '/':
path = os.path.join(loc, path)
# Always use long-path notation
path = long_path(path)
else:
path = loc
return os.path.normpath(os.path.abspath(path))
def create_real_path(name, loc, path, umask=False, writable=True):
""" When 'path' is relative, create join of 'loc' and 'path'
When 'path' is absolute, create normalized path
'name' is used for logging.
Optional 'umask' will be applied.
'writable' means that an existing folder should be writable
Returns ('success', 'full path')
"""
if path:
my_dir = real_path(loc, path)
if not os.path.exists(my_dir):
logging.info('%s directory: %s does not exist, try to create it', name, my_dir)
if not create_all_dirs(my_dir, umask):
logging.error(T('Cannot create directory %s'), clip_path(my_dir))
return (False, my_dir)
checks = (os.W_OK + os.R_OK) if writable else os.R_OK
if os.access(my_dir, checks):
return (True, my_dir)
else:
logging.error(T('%s directory: %s error accessing'), name, clip_path(my_dir))
return (False, my_dir)
else:
return (False, "")
def is_relative_path(p):
""" Return True if path is relative """
p = p.replace('\\', '/')
if p and p[0] == '/':
return False
if sabnzbd.WIN32 and p and len(p) > 2:
if p[0].isalpha() and p[1] == ':' and p[2] == '/':
return False
return True
def same_file(a, b):
""" Return 0 if A and B have nothing in common
return 1 if A and B are actually the same path
return 2 if B is a subfolder of A
"""
a = os.path.normpath(os.path.abspath(a))
b = os.path.normpath(os.path.abspath(b))
if sabnzbd.WIN32 or sabnzbd.DARWIN:
a = a.lower()
b = b.lower()
if b.startswith(a):
return 2
if "samefile" in os.path.__dict__:
try:
return int(os.path.samefile(a, b))
except:
return 0
else:
return int(a == b)
def check_mount(path):
""" Return False if volume isn't mounted on Linux or OSX
Retry 6 times with an interval of 1 sec.
"""
if sabnzbd.DARWIN:
m = re.search(r'^(/Volumes/[^/]+)/', path, re.I)
elif sabnzbd.WIN32:
m = re.search(r'^([a-z]:\\)', path, re.I)
else:
m = re.search(r'^(/(?:mnt|media)/[^/]+)/', path)
if m:
for n in range(sabnzbd.cfg.wait_ext_drive() or 1):
if os.path.exists(m.group(1)):
return True
logging.debug('Waiting for %s to come online', m.group(1))
time.sleep(1)
return not m
def safe_fnmatch(f, pattern):
""" fnmatch will fail if the pattern contains any of it's
key characters, like [, ] or !.
"""
try:
return fnmatch.fnmatch(f, pattern)
except re.error:
return False
def globber(path, pattern='*'):
""" Return matching base file/folder names in folder `path` """
# Cannot use glob.glob() because it doesn't support Windows long name notation
if os.path.exists(path):
return [f for f in os.listdir(path) if safe_fnmatch(f, pattern)]
return []
def globber_full(path, pattern='*'):
""" Return matching full file/folder names in folder `path` """
# Cannot use glob.glob() because it doesn't support Windows long name notation
if os.path.exists(path):
return [os.path.join(path, f) for f in os.listdir(path) if safe_fnmatch(f, pattern)]
return []
def trim_win_path(path):
""" Make sure Windows path stays below 70 by trimming last part """
if sabnzbd.WIN32 and len(path) > 69:
path, folder = os.path.split(path)
maxlen = 69 - len(path)
if len(folder) > maxlen:
folder = folder[:maxlen]
path = os.path.join(path, folder).rstrip('. ')
return path
def fix_unix_encoding(folder):
""" Fix bad name encoding for Unix systems """
if not sabnzbd.WIN32 and not sabnzbd.DARWIN and gUTF:
for root, dirs, files in os.walk(folder.encode('utf-8')):
for name in files:
new_name = special_fixer(name).encode('utf-8')
if name != new_name:
try:
shutil.move(os.path.join(root, name), os.path.join(root, new_name))
except:
logging.info('Cannot correct name of %s', os.path.join(root, name))
def make_script_path(script):
""" Return full script path, if any valid script exists, else None """
s_path = None
path = sabnzbd.cfg.script_dir.get_path()
if path and script:
if script.lower() not in ('none', 'default'):
s_path = os.path.join(path, script)
if not os.path.exists(s_path):
s_path = None
return s_path
def get_admin_path(name, future):
""" Return news-style full path to job-admin folder of names job
or else the old cache path
"""
if future:
return os.path.join(sabnzbd.cfg.admin_dir.get_path(), FUTURE_Q_FOLDER)
else:
return os.path.join(os.path.join(sabnzbd.cfg.download_dir.get_path(), name), JOB_ADMIN)
def starts_with_path(path, prefix):
""" Return True if 'path' starts with 'prefix',
considering case-sensitivity of the file system
"""
if sabnzbd.WIN32:
return clip_path(path).lower().startswith(prefix.lower())
elif sabnzbd.DARWIN:
return path.lower().startswith(prefix.lower())
else:
return path.startswith(prefix)
def set_chmod(path, permissions, report):
""" Set 'permissions' on 'path', report any errors when 'report' is True """
try:
logging.debug('Applying permissions %s (octal) to %s', oct(permissions), path)
os.chmod(path, permissions)
except:
lpath = path.lower()
if report and '.appledouble' not in lpath and '.ds_store' not in lpath:
logging.error(T('Cannot change permissions of %s'), clip_path(path))
logging.info("Traceback: ", exc_info=True)
def set_permissions(path, recursive=True):
""" Give folder tree and its files their proper permissions """
if not sabnzbd.WIN32:
umask = sabnzbd.cfg.umask()
try:
# Make sure that user R+W+X is on
umask = int(umask, 8) | int('0700', 8)
report = True
except ValueError:
# No or no valid permissions
# Use the effective permissions of the session
# Don't report errors (because the system might not support it)
umask = int('0777', 8) & (sabnzbd.ORG_UMASK ^ int('0777', 8))
report = False
# Remove X bits for files
umask_file = umask & int('7666', 8)
if os.path.isdir(path):
if recursive:
# Parse the dir/file tree and set permissions
for root, _dirs, files in os.walk(path):
set_chmod(root, umask, report)
for name in files:
set_chmod(os.path.join(root, name), umask_file, report)
else:
set_chmod(path, umask, report)
else:
set_chmod(path, umask_file, report)
def clip_path(path):
r""" Remove \\?\ or \\?\UNC\ prefix from Windows path """
if sabnzbd.WIN32 and path and '?' in path:
path = path.replace('\\\\?\\UNC\\', '\\\\', 1).replace('\\\\?\\', '', 1)
return path
def long_path(path):
""" For Windows, convert to long style path; others, return same path """
if sabnzbd.WIN32 and path and not path.startswith('\\\\?\\'):
if path.startswith('\\\\'):
# Special form for UNC paths
path = path.replace('\\\\', '\\\\?\\UNC\\', 1)
else:
# Normal form for local paths
path = '\\\\?\\' + path
return path
##############################################################################
# Locked directory operations to avoid problems with simultaneous add/remove
##############################################################################
DIR_LOCK = threading.RLock()
@synchronized(DIR_LOCK)
def get_unique_path(dirpath, n=0, create_dir=True):
""" Determine a unique folder or filename """
if not check_mount(dirpath):
return dirpath
path = dirpath
if n:
path = "%s.%s" % (dirpath, n)
if not os.path.exists(path):
if create_dir:
return create_dirs(path)
else:
return path
else:
return get_unique_path(dirpath, n=n + 1, create_dir=create_dir)
@synchronized(DIR_LOCK)
def get_unique_filename(path):
""" Check if path is unique.
If not, add number like: "/path/name.NUM.ext".
"""
num = 1
new_path, fname = os.path.split(path)
name, ext = os.path.splitext(fname)
while os.path.exists(path):
fname = "%s.%d%s" % (name, num, ext)
num += 1
path = os.path.join(new_path, fname)
return path
@synchronized(DIR_LOCK)
def create_dirs(dirpath):
""" Create directory tree, obeying permissions """
if not os.path.exists(dirpath):
logging.info('Creating directories: %s', dirpath)
if not create_all_dirs(dirpath, True):
logging.error(T('Failed making (%s)'), clip_path(dirpath))
return None
return dirpath
@synchronized(DIR_LOCK)
def move_to_path(path, new_path):
""" Move a file to a new path, optionally give unique filename
Return (ok, new_path)
"""
ok = True
overwrite = sabnzbd.cfg.overwrite_files()
new_path = os.path.abspath(new_path)
if overwrite and os.path.exists(new_path):
try:
os.remove(new_path)
except:
overwrite = False
if not overwrite:
new_path = get_unique_filename(new_path)
if new_path:
logging.debug("Moving (overwrite: %s) %s => %s", overwrite, path, new_path)
try:
# First try cheap rename
renamer(path, new_path)
except:
# Cannot rename, try copying
logging.debug("File could not be renamed, trying copying: %s", path)
try:
if not os.path.exists(os.path.dirname(new_path)):
create_dirs(os.path.dirname(new_path))
shutil.copyfile(path, new_path)
os.remove(path)
except:
# Check if the old-file actually exists (possible delete-delays)
if not os.path.exists(path):
logging.debug("File not moved, original path gone: %s", path)
return True, None
if not (sabnzbd.cfg.marker_file() and sabnzbd.cfg.marker_file() in path):
logging.error(T('Failed moving %s to %s'), clip_path(path), clip_path(new_path))
logging.info("Traceback: ", exc_info=True)
ok = False
return ok, new_path
@synchronized(DIR_LOCK)
def cleanup_empty_directories(path):
""" Remove all empty folders inside (and including) 'path' """
path = os.path.normpath(path)
while 1:
repeat = False
for root, dirs, files in os.walk(path, topdown=False):
if not dirs and not files and root != path:
try:
remove_dir(root)
repeat = True
except:
pass
if not repeat:
break
try:
remove_dir(path)
except:
pass
@synchronized(DIR_LOCK)
def get_filepath(path, nzo, filename):
""" Create unique filepath """
# This procedure is only used by the Assembler thread
# It does no umask setting
# It uses the dir_lock for the (rare) case that the
# download_dir is equal to the complete_dir.
dName = nzo.work_name
if not nzo.created:
for n in range(200):
dName = dirname
if n:
dName += '.' + str(n)
try:
os.mkdir(os.path.join(path, dName))
break
except:
pass
nzo.work_name = dName
nzo.created = True
fPath = os.path.join(os.path.join(path, dName), filename)
fPath, ext = os.path.splitext(fPath)
n = 0
while True:
if n:
fullPath = "%s.%d%s" % (fPath, n, ext)
else:
fullPath = fPath + ext
if os.path.exists(fullPath):
n = n + 1
else:
break
return fullPath
@synchronized(DIR_LOCK)
def renamer(old, new):
""" Rename file/folder with retries for Win32 """
# Sanitize last part of new name
path, name = os.path.split(new)
# Use the more stringent folder rename to end up with a nicer name,
# but do not trim size
new = os.path.join(path, sanitize_foldername(name, False))
logging.debug('Renaming "%s" to "%s"', old, new)
if sabnzbd.WIN32:
retries = 15
while retries > 0:
# First we try 3 times with os.rename
if retries > 12:
try:
os.rename(old, new)
return
except:
retries -= 1
time.sleep(3)
continue
# Now we try the back-up method
logging.debug('Could not rename, trying move for %s to %s', old, new)
try:
shutil.move(old, new)
return
except WindowsError as err:
logging.debug('Error renaming "%s" to "%s" <%s>', old, new, err)
if err[0] == 32:
logging.debug('Retry rename %s to %s', old, new)
retries -= 1
else:
raise WindowsError(err)
time.sleep(3)
raise WindowsError(err)
else:
shutil.move(old, new)
@synchronized(DIR_LOCK)
def remove_dir(path):
""" Remove directory with retries for Win32 """
logging.debug('Removing dir %s', path)
if sabnzbd.WIN32:
retries = 15
while retries > 0:
try:
os.rmdir(path)
return
except WindowsError as err:
if err[0] == 32:
logging.debug('Retry delete %s', path)
retries -= 1
else:
raise WindowsError(err)
time.sleep(3)
raise WindowsError(err)
else:
os.rmdir(path)
@synchronized(DIR_LOCK)
def remove_all(path, pattern='*', keep_folder=False, recursive=False):
""" Remove folder and all its content (optionally recursive) """
if os.path.exists(path):
files = globber_full(path, pattern)
if pattern == '*' and not sabnzbd.WIN32:
files.extend(globber_full(path, '.*'))
for f in files:
if os.path.isfile(f):
try:
logging.debug('Removing file %s', f)
os.remove(f)
except:
logging.info('Cannot remove file %s', f)
elif recursive:
remove_all(f, pattern, False, True)
if not keep_folder:
try:
logging.debug('Removing dir %s', path)
os.rmdir(path)
except:
logging.info('Cannot remove folder %s', path)
##############################################################################
# Diskfree
##############################################################################
def find_dir(p):
""" Return first folder level that exists in this path """
x = 'x'
while x and not os.path.exists(p):
p, x = os.path.split(p)
return p
if sabnzbd.WIN32:
# windows diskfree
try:
# Careful here, because win32api test hasn't been done yet!
import win32api
except:
pass
def diskspace_base(_dir):
""" Return amount of free and used diskspace in GBytes """
_dir = find_dir(_dir)
try:
available, disk_size, total_free = win32api.GetDiskFreeSpaceEx(_dir)
return disk_size / GIGI, available / GIGI
except:
return 0.0, 0.0
else:
try:
os.statvfs
# posix diskfree
def diskspace_base(_dir):
""" Return amount of free and used diskspace in GBytes """
_dir = find_dir(_dir)
try:
s = os.statvfs(_dir)
if s.f_blocks < 0:
disk_size = float(sys.maxsize) * float(s.f_frsize)
else:
disk_size = float(s.f_blocks) * float(s.f_frsize)
if s.f_bavail < 0:
available = float(sys.maxsize) * float(s.f_frsize)
else:
available = float(s.f_bavail) * float(s.f_frsize)
return disk_size / GIGI, available / GIGI
except:
return 0.0, 0.0
except ImportError:
def diskspace_base(_dir):
return 20.0, 10.0
# Store all results to speed things up
__DIRS_CHECKED = []
__DISKS_SAME = None
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []}
__LAST_DISK_CALL = 0
def diskspace(force=False):
""" Wrapper to cache results """
global __DIRS_CHECKED, __DISKS_SAME, __LAST_DISK_RESULT, __LAST_DISK_CALL
# Reset everything when folders changed
dirs_to_check = [sabnzbd.cfg.download_dir.get_path(), sabnzbd.cfg.complete_dir.get_path()]
if __DIRS_CHECKED != dirs_to_check:
__DIRS_CHECKED = dirs_to_check
__DISKS_SAME = None
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []}
__LAST_DISK_CALL = 0
# When forced, ignore any cache to avoid problems in UI
if force:
__LAST_DISK_CALL = 0
# Check against cache
if time.time() > __LAST_DISK_CALL + 10.0:
# Same disk? Then copy-paste
__LAST_DISK_RESULT['download_dir'] = diskspace_base(sabnzbd.cfg.download_dir.get_path())
__LAST_DISK_RESULT['complete_dir'] = __LAST_DISK_RESULT['download_dir'] if __DISKS_SAME else diskspace_base(sabnzbd.cfg.complete_dir.get_path())
__LAST_DISK_CALL = time.time()
# Do we know if it's same disk?
if __DISKS_SAME is None:
__DISKS_SAME = (__LAST_DISK_RESULT['download_dir'] == __LAST_DISK_RESULT['complete_dir'])
return __LAST_DISK_RESULT

7
sabnzbd/getipaddress.py

@ -20,11 +20,12 @@ sabnzbd.getipaddress
"""
import socket
import sabnzbd
import sabnzbd.cfg
import multiprocessing.pool
import functools
import sabnzbd
import sabnzbd.cfg
from sabnzbd.encoding import ubtou
# decorator stuff:
def timeout(max_timeout):
@ -92,7 +93,7 @@ def publicipv4():
# specify the Host, because we only provide the IPv4 address in the URL:
req.add_header('Host', sabnzbd.cfg.selftest_host())
# get the response, timeout 2 seconds, in case the website is not accessible
public_ipv4 = urllib.request.urlopen(req, timeout=2).read().decode('utf-8')
public_ipv4 = ubtou(urllib.request.urlopen(req, timeout=2).read())
# ... check the response is indeed an IPv4 address:
socket.inet_aton(public_ipv4) # if we got anything else than a plain IPv4 address, this will raise an exception
# if we get here without exception, we're done:

6
sabnzbd/interface.py

@ -38,9 +38,9 @@ import sabnzbd.rss
import sabnzbd.scheduler as scheduler
from Cheetah.Template import Template
from sabnzbd.misc import real_path, to_units, from_units, \
time_format, long_path, calc_age, same_file, \
cat_to_opts, int_conv, globber, globber_full, remove_all, get_base_url
from sabnzbd.misc import to_units, from_units, time_format, calc_age, \
cat_to_opts, int_conv, get_base_url
from sabnzbd.filesystem import real_path, long_path, globber, globber_full, remove_all
from sabnzbd.newswrapper import GetServerParms
from sabnzbd.rating import Rating
from sabnzbd.bpsmeter import BPSMeter

857
sabnzbd/misc.py

@ -102,7 +102,7 @@ def monthrange(start, finish):
for i in range(start.month, months):
year = (i - 1) / 12 + start.year
month = (i - 1) % 12 + 1
yield datetime.date(year, month, 1)
yield datetime.date(int(year), int(month), 1)
def safe_lower(txt):
@ -113,36 +113,6 @@ def safe_lower(txt):
return ''
def safe_fnmatch(f, pattern):
""" fnmatch will fail if the pattern contains any of it's
key characters, like [, ] or !.
"""
try:
return fnmatch.fnmatch(f, pattern)
except re.error:
return False
def globber(path, pattern='*'):
""" Return matching base file/folder names in folder `path` """
# Cannot use glob.glob() because it doesn't support Windows long name notation
if os.path.exists(path):
return [f for f in os.listdir(path) if safe_fnmatch(f, pattern)]
return []
def globber_full(path, pattern='*'):
""" Return matching full file/folder names in folder `path` """
# Cannot use glob.glob() because it doesn't support Windows long name notation
if os.path.exists(path):
try:
return [os.path.join(path, f) for f in os.listdir(path) if safe_fnmatch(f, pattern)]
except UnicodeDecodeError:
# This happens on Linux when names are incorrectly encoded, retry using a non-Unicode path
path = path.encode('utf-8')
return [os.path.join(path, f) for f in os.listdir(path) if safe_fnmatch(f, pattern)]
return []
def cat_to_opts(cat, pp=None, script=None, priority=None):
""" Derive options from category, if options not already defined.
@ -240,298 +210,6 @@ def cat_convert(cat):
return None
##############################################################################
# sanitize_filename
##############################################################################
_DEVICES = ('con', 'prn', 'aux', 'nul',
'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9')
def replace_win_devices(name):
''' Remove reserved Windows device names from a name.
aux.txt ==> _aux.txt
txt.aux ==> txt.aux
'''
if name:
lname = name.lower()
for dev in _DEVICES:
if lname == dev or lname.startswith(dev + '.'):
name = '_' + name
break
# Remove special NTFS filename
if lname.startswith('$mft'):
name = name.replace('$', 'S', 1)
return name
def has_win_device(p):
""" Return True if filename part contains forbidden name
Before and after sanitizing
"""
p = os.path.split(p)[1].lower()
for dev in _DEVICES:
if p == dev or p.startswith(dev + '.') or p.startswith('_' + dev + '.'):
return True
return False
if sabnzbd.WIN32:
# the colon should be here too, but we'll handle that separately
CH_ILLEGAL = '\/<>?*|"\t'
CH_LEGAL = '++{}!@#`+'
else:
CH_ILLEGAL = '/'
CH_LEGAL = '+'
def sanitize_filename(name):
""" Return filename with illegal chars converted to legal ones
and with the par2 extension always in lowercase
"""
if not name:
return name
illegal = CH_ILLEGAL
legal = CH_LEGAL
if ':' in name:
if sabnzbd.WIN32:
# Compensate for the odd way par2 on Windows substitutes a colon character
name = name.replace(':', '3A')
elif sabnzbd.DARWIN:
# Compensate for the foolish way par2 on OSX handles a colon character
name = name[name.rfind(':') + 1:]
if sabnzbd.WIN32 or cfg.sanitize_safe():
name = replace_win_devices(name)
lst = []
for ch in name.strip():
if ch in illegal:
ch = legal[illegal.find(ch)]
lst.append(ch)
name = ''.join(lst)
if not name:
name = 'unknown'
name, ext = os.path.splitext(name)
lowext = ext.lower()
if lowext == '.par2' and lowext != ext:
ext = lowext
return name + ext
def sanitize_foldername(name, limit=True):
""" Return foldername with dodgy chars converted to safe ones
Remove any leading and trailing dot and space characters
"""
if not name:
return name
illegal = CH_ILLEGAL + ':\x92"'
legal = CH_LEGAL + "-''"
if cfg.sanitize_safe():
# Remove all bad Windows chars too
illegal += r'\/<>?*|":'
legal += r'++{}!@#`;'
repl = cfg.replace_illegal()
lst = []
for ch in name.strip():
if ch in illegal:
if repl:
ch = legal[illegal.find(ch)]
lst.append(ch)
else:
lst.append(ch)
name = ''.join(lst)
name = name.strip()
if sabnzbd.WIN32 or cfg.sanitize_safe():
name = replace_win_devices(name)
maxlen = cfg.folder_max_length()
if limit and len(name) > maxlen:
name = name[:maxlen]
# And finally, make sure it doesn't end in a dot
if name != '.' and name != '..':
name = name.rstrip('.')
if not name:
name = 'unknown'
return name
def sanitize_and_trim_path(path):
""" Remove illegal characters and trim element size """
path = path.strip()
new_path = ''
if sabnzbd.WIN32:
if path.startswith('\\\\?\\UNC\\'):
new_path = '\\\\?\\UNC\\'
path = path[8:]
elif path.startswith('\\\\?\\'):
new_path = '\\\\?\\'
path = path[4:]
path = path.replace('\\', '/')
parts = path.split('/')
if sabnzbd.WIN32 and len(parts[0]) == 2 and ':' in parts[0]:
new_path += parts[0] + '/'
parts.pop(0)
elif path.startswith('//'):
new_path = '//'
elif path.startswith('/'):
new_path = '/'
for part in parts:
new_path = os.path.join(new_path, sanitize_foldername(part))
return os.path.abspath(os.path.normpath(new_path))
def sanitize_files_in_folder(folder):
""" Sanitize each file in the folder, return list of new names
"""
lst = []
for root, _, files in os.walk(folder):
for file_ in files:
path = os.path.join(root, file_)
new_path = os.path.join(root, sanitize_filename(file_))
if path != new_path:
try:
os.rename(path, new_path)
path = new_path
except:
logging.debug('Cannot rename %s to %s', path, new_path)
lst.append(path)
return lst
def is_obfuscated_filename(filename):
""" Check if this file has an extension, if not, it's
probably obfuscated and we don't use it
"""
return (os.path.splitext(filename)[1] == '')
##############################################################################
# DirPermissions
##############################################################################
def create_all_dirs(path, umask=False):
""" Create all required path elements and set umask on all
Return True if last element could be made or exists
"""
result = True
if sabnzbd.WIN32:
try:
os.makedirs(path)
except:
result = False
else:
lst = []
lst.extend(path.split('/'))
path = ''
for d in lst:
if d:
path += '/' + d
if not os.path.exists(path):
try:
os.mkdir(path)
result = True
except:
result = False
if umask:
mask = cfg.umask()
if mask:
try:
os.chmod(path, int(mask, 8) | 0o700)
except:
pass
return result
##############################################################################
# Real_Path
##############################################################################
def real_path(loc, path):
""" When 'path' is relative, return normalized join of 'loc' and 'path'
When 'path' is absolute, return normalized path
A path starting with ~ will be located in the user's Home folder
"""
# The Windows part is a bit convoluted because
# os.path.join() doesn't behave the same for all Python versions
if path:
path = path.strip()
else:
path = ''
if path:
if not sabnzbd.WIN32 and path.startswith('~/'):
path = path.replace('~', os.environ.get('HOME', sabnzbd.DIR_HOME), 1)
if sabnzbd.WIN32:
path = path.replace('/', '\\')
if len(path) > 1 and path[0].isalpha() and path[1] == ':':
if len(path) == 2 or path[2] != '\\':
path = path.replace(':', ':\\', 1)
elif path.startswith('\\\\'):
pass
elif path.startswith('\\'):
if len(loc) > 1 and loc[0].isalpha() and loc[1] == ':':
path = loc[:2] + path
else:
path = os.path.join(loc, path)
elif path[0] != '/':
path = os.path.join(loc, path)
# Always use long-path notation
path = long_path(path)
else:
path = loc
return os.path.normpath(os.path.abspath(path))
##############################################################################
# Create_Real_Path
##############################################################################
def create_real_path(name, loc, path, umask=False, writable=True):
""" When 'path' is relative, create join of 'loc' and 'path'
When 'path' is absolute, create normalized path
'name' is used for logging.
Optional 'umask' will be applied.
'writable' means that an existing folder should be writable
Returns ('success', 'full path')
"""
if path:
my_dir = real_path(loc, path)
if not os.path.exists(my_dir):
logging.info('%s directory: %s does not exist, try to create it', name, my_dir)
if not create_all_dirs(my_dir, umask):
logging.error(T('Cannot create directory %s'), clip_path(my_dir))
return (False, my_dir)
checks = (os.W_OK + os.R_OK) if writable else os.R_OK
if os.access(my_dir, checks):
return (True, my_dir)
else:
logging.error(T('%s directory: %s error accessing'), name, clip_path(my_dir))
return (False, my_dir)
else:
return (False, "")
def is_relative_path(p):
""" Return True if path is relative """
p = p.replace('\\', '/')
if p and p[0] == '/':
return False
if sabnzbd.WIN32 and p and len(p) > 2:
if p[0].isalpha() and p[1] == ':' and p[2] == '/':
return False
return True
def windows_variant():
""" Determine Windows variant
Return vista_plus, x64
@ -790,28 +468,6 @@ def caller_name(skip=2):
return ".".join([module_name, function_name])
def same_file(a, b):
""" Return 0 if A and B have nothing in common
return 1 if A and B are actually the same path
return 2 if B is a subfolder of A
"""
a = os.path.normpath(os.path.abspath(a))
b = os.path.normpath(os.path.abspath(b))
if sabnzbd.WIN32 or sabnzbd.DARWIN:
a = a.lower()
b = b.lower()
if b.startswith(a):
return 2
if "samefile" in os.path.__dict__:
try:
return int(os.path.samefile(a, b))
except:
return 0
else:
return int(a == b)
def exit_sab(value):
""" Leave the program after flushing stderr/stdout """
sys.stderr.flush()
@ -840,34 +496,6 @@ def split_host(srv):
return (host, port)
def get_from_url(url):
""" Retrieve URL and return content """
try:
return urllib2.urlopen(url).read()
except:
return None
def check_mount(path):
""" Return False if volume isn't mounted on Linux or OSX
Retry 6 times with an interval of 1 sec.
"""
if sabnzbd.DARWIN:
m = re.search(r'^(/Volumes/[^/]+)/', path, re.I)
elif sabnzbd.WIN32:
m = re.search(r'^([a-z]:\\)', path, re.I)
else:
m = re.search(r'^(/(?:mnt|media)/[^/]+)/', path)
if m:
for n in range(cfg.wait_ext_drive() or 1):
if os.path.exists(m.group(1)):
return True
logging.debug('Waiting for %s to come online', m.group(1))
time.sleep(1)
return not m
def get_cache_limit():
""" Depending on OS, calculate cache limit """
# OSX/Windows use Default value
@ -890,286 +518,6 @@ def get_cache_limit():
return ''
##############################################################################
# Locked directory operations to avoid problems with simultaneous add/remove
##############################################################################
DIR_LOCK = threading.RLock()
@synchronized(DIR_LOCK)
def get_unique_path(dirpath, n=0, create_dir=True):
""" Determine a unique folder or filename """
if not check_mount(dirpath):
return dirpath
path = dirpath
if n:
path = "%s.%s" % (dirpath, n)
if not os.path.exists(path):
if create_dir:
return create_dirs(path)
else:
return path
else:
return get_unique_path(dirpath, n=n + 1, create_dir=create_dir)
@synchronized(DIR_LOCK)
def get_unique_filename(path):
""" Check if path is unique.
If not, add number like: "/path/name.NUM.ext".
"""
num = 1
new_path, fname = os.path.split(path)
name, ext = os.path.splitext(fname)
while os.path.exists(path):
fname = "%s.%d%s" % (name, num, ext)
num += 1
path = os.path.join(new_path, fname)
return path
@synchronized(DIR_LOCK)
def create_dirs(dirpath):
""" Create directory tree, obeying permissions """
if not os.path.exists(dirpath):
logging.info('Creating directories: %s', dirpath)
if not create_all_dirs(dirpath, True):
logging.error(T('Failed making (%s)'), clip_path(dirpath))
return None
return dirpath
@synchronized(DIR_LOCK)
def move_to_path(path, new_path):
""" Move a file to a new path, optionally give unique filename
Return (ok, new_path)
"""
ok = True
overwrite = cfg.overwrite_files()
new_path = os.path.abspath(new_path)
if overwrite and os.path.exists(new_path):
try:
remove_file(new_path)
except:
overwrite = False
if not overwrite:
new_path = get_unique_filename(new_path)
if new_path:
logging.debug("Moving (overwrite: %s) %s => %s", overwrite, path, new_path)
try:
# First try cheap rename
renamer(path, new_path)
except:
# Cannot rename, try copying
logging.debug("File could not be renamed, trying copying: %s", path)
try:
if not os.path.exists(os.path.dirname(new_path)):
create_dirs(os.path.dirname(new_path))
shutil.copyfile(path, new_path)
remove_file(path)
except:
# Check if the old-file actually exists (possible delete-delays)
if not os.path.exists(path):
logging.debug("File not moved, original path gone: %s", path)
return True, None
if not (cfg.marker_file() and cfg.marker_file() in path):
logging.error(T('Failed moving %s to %s'), clip_path(path), clip_path(new_path))
logging.info("Traceback: ", exc_info=True)
ok = False
return ok, new_path
@synchronized(DIR_LOCK)
def cleanup_empty_directories(path):
""" Remove all empty folders inside (and including) 'path' """
path = os.path.normpath(path)
while 1:
repeat = False
for root, dirs, files in os.walk(path, topdown=False):
if not dirs and not files and root != path:
try:
remove_dir(root)
repeat = True
except:
pass
if not repeat:
break
try:
remove_dir(path)
except:
pass
@synchronized(DIR_LOCK)
def get_filepath(path, nzo, filename):
""" Create unique filepath """
# This procedure is only used by the Assembler thread
# It does no umask setting
# It uses the dir_lock for the (rare) case that the
# download_dir is equal to the complete_dir.
dName = nzo.work_name
if not nzo.created:
for n in range(200):
dName = dirname
if n:
dName += '.' + str(n)
try:
os.mkdir(os.path.join(path, dName))
break
except:
pass
nzo.work_name = dName
nzo.created = True
fPath = os.path.join(os.path.join(path, dName), filename)
fPath, ext = os.path.splitext(fPath)
n = 0
while True:
if n:
fullPath = "%s.%d%s" % (fPath, n, ext)
else:
fullPath = fPath + ext
if os.path.exists(fullPath):
n = n + 1
else:
break
return fullPath
@synchronized(DIR_LOCK)
def renamer(old, new):
""" Rename file/folder with retries for Win32 """
# Sanitize last part of new name
path, name = os.path.split(new)
# Use the more stringent folder rename to end up with a nicer name,
# but do not trim size
new = os.path.join(path, sanitize_foldername(name, False))
logging.debug('Renaming "%s" to "%s"', old, new)
if sabnzbd.WIN32:
retries = 15
while retries > 0:
# First we try 3 times with os.rename
if retries > 12:
try:
os.rename(old, new)
return
except:
retries -= 1
time.sleep(3)
continue
# Now we try the back-up method
logging.debug('Could not rename, trying move for %s to %s', old, new)
try:
shutil.move(old, new)
return
except WindowsError as err:
logging.debug('Error renaming "%s" to "%s" <%s>', old, new, err)
if err[0] == 32:
logging.debug('Retry rename %s to %s', old, new)
retries -= 1
else:
raise WindowsError(err)
time.sleep(3)
raise WindowsError(err)
else:
shutil.move(old, new)
@synchronized(DIR_LOCK)
def remove_dir(path):
""" Remove directory with retries for Win32 """
if sabnzbd.WIN32:
retries = 15
while retries > 0:
try:
remove_dir(path)
return
except WindowsError as err:
if err[0] == 32:
logging.debug('Retry delete %s', path)
retries -= 1
else:
raise WindowsError(err)
time.sleep(3)
raise WindowsError(err)
else:
remove_dir(path)
@synchronized(DIR_LOCK)
def remove_all(path, pattern='*', keep_folder=False, recursive=False):
""" Remove folder and all its content (optionally recursive) """
if os.path.exists(path):
files = globber_full(path, pattern)
if pattern == '*' and not sabnzbd.WIN32:
files.extend(globber_full(path, '.*'))
for f in files:
if os.path.isfile(f):
try:
remove_file(f)
except:
logging.info('Cannot remove file %s', f)
elif recursive:
remove_all(f, pattern, False, True)
if not keep_folder:
try:
remove_dir(path)
except:
logging.info('Cannot remove folder %s', path)
def remove_file(path):
""" Wrapper function so any file removal is logged """
logging.debug('[%s] Deleting file %s', caller_name(), path)
os.remove(path)
def remove_dir(dir):
""" Wrapper function so any dir removal is logged """
logging.debug('[%s] Deleting dir %s', caller_name(), dir)
os.rmdir(dir)
def trim_win_path(path):
""" Make sure Windows path stays below 70 by trimming last part """
if sabnzbd.WIN32 and len(path) > 69:
path, folder = os.path.split(path)
maxlen = 69 - len(path)
if len(folder) > maxlen:
folder = folder[:maxlen]
path = os.path.join(path, folder).rstrip('. ')
return path
def make_script_path(script):
""" Return full script path, if any valid script exists, else None """
s_path = None
path = cfg.script_dir.get_path()
if path and script:
if script.lower() not in ('none', 'default'):
s_path = os.path.join(path, script)
if not os.path.exists(s_path):
s_path = None
return s_path
def get_admin_path(name, future):
""" Return news-style full path to job-admin folder of names job
or else the old cache path
"""
if future:
return os.path.join(cfg.admin_dir.get_path(), FUTURE_Q_FOLDER)
else:
return os.path.join(os.path.join(cfg.download_dir.get_path(), name), JOB_ADMIN)
def on_cleanup_list(filename, skip_nzb=False):
""" Return True if a filename matches the clean-up list """
lst = cfg.cleanup_list()
@ -1185,22 +533,6 @@ def on_cleanup_list(filename, skip_nzb=False):
return False
def get_ext(filename):
""" Return lowercased file extension """
try:
return os.path.splitext(filename)[1].lower()
except:
return ''
def get_filename(path):
""" Return path without the file extension """
try:
return os.path.split(path)[1]
except:
return ''
def memory_usage():
try:
# Probably only works on Linux because it uses /proc/<pid>/statm
@ -1275,98 +607,6 @@ def int_conv(value):
return value
##############################################################################
# Diskfree
##############################################################################
def find_dir(p):
""" Return first folder level that exists in this path """
x = 'x'
while x and not os.path.exists(p):
p, x = os.path.split(p)
return p
if sabnzbd.WIN32:
# windows diskfree
try:
# Careful here, because win32api test hasn't been done yet!
import win32api
except:
pass
def diskspace_base(_dir):
""" Return amount of free and used diskspace in GBytes """
_dir = find_dir(_dir)
try:
available, disk_size, total_free = win32api.GetDiskFreeSpaceEx(_dir)
return disk_size / GIGI, available / GIGI
except:
return 0.0, 0.0
else:
try:
os.statvfs
# posix diskfree
def diskspace_base(_dir):
""" Return amount of free and used diskspace in GBytes """
_dir = find_dir(_dir)
try:
s = os.statvfs(_dir)
if s.f_blocks < 0:
disk_size = float(sys.maxsize) * float(s.f_frsize)
else:
disk_size = float(s.f_blocks) * float(s.f_frsize)
if s.f_bavail < 0:
available = float(sys.maxsize) * float(s.f_frsize)
else:
available = float(s.f_bavail) * float(s.f_frsize)
return disk_size / GIGI, available / GIGI
except:
return 0.0, 0.0
except ImportError:
def diskspace_base(_dir):
return 20.0, 10.0
# Store all results to speed things up
__DIRS_CHECKED = []
__DISKS_SAME = None
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []}
__LAST_DISK_CALL = 0
def diskspace(force=False):
""" Wrapper to cache results """
global __DIRS_CHECKED, __DISKS_SAME, __LAST_DISK_RESULT, __LAST_DISK_CALL
# Reset everything when folders changed
dirs_to_check = [cfg.download_dir.get_path(), cfg.complete_dir.get_path()]
if __DIRS_CHECKED != dirs_to_check:
__DIRS_CHECKED = dirs_to_check
__DISKS_SAME = None
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []}
__LAST_DISK_CALL = 0
# When forced, ignore any cache to avoid problems in UI
if force:
__LAST_DISK_CALL = 0
# Check against cache
if time.time() > __LAST_DISK_CALL + 10.0:
# Same disk? Then copy-paste
__LAST_DISK_RESULT['download_dir'] = diskspace_base(cfg.download_dir.get_path())
__LAST_DISK_RESULT['complete_dir'] = __LAST_DISK_RESULT['download_dir'] if __DISKS_SAME else diskspace_base(cfg.complete_dir.get_path())
__LAST_DISK_CALL = time.time()
# Do we know if it's same disk?
if __DISKS_SAME is None:
__DISKS_SAME = (__LAST_DISK_RESULT['download_dir'] == __LAST_DISK_RESULT['complete_dir'])
return __LAST_DISK_RESULT
##############################################################################
# Other support functions
##############################################################################
def create_https_certificates(ssl_cert, ssl_key):
""" Create self-signed HTTPS certificates and store in paths 'ssl_cert' and 'ssl_key' """
if not sabnzbd.HAVE_CRYPTOGRAPHY:
@ -1487,14 +727,6 @@ def ip_extract():
return ips
def is_writable(path):
""" Return True is file is writable (also when non-existent) """
if os.path.isfile(path):
return bool(os.stat(path).st_mode & stat.S_IWUSR)
else:
return True
def get_base_url(url):
""" Return only the true root domain for the favicon, so api.oznzb.com -> oznzb.com
But also api.althub.co.za -> althub.co.za
@ -1518,93 +750,6 @@ def match_str(text, matches):
return None
def starts_with_path(path, prefix):
""" Return True if 'path' starts with 'prefix',
considering case-sensitivity of the file system
"""
if sabnzbd.WIN32:
return clip_path(path).lower().startswith(prefix.lower())
elif sabnzbd.DARWIN:
return path.lower().startswith(prefix.lower())
else:
return path.startswith(prefix)
def set_chmod(path, permissions, report):
""" Set 'permissions' on 'path', report any errors when 'report' is True """
try:
logging.debug('Applying permissions %s (octal) to %s', oct(permissions), path)
os.chmod(path, permissions)
except:
lpath = path.lower()
if report and '.appledouble' not in lpath and '.ds_store' not in lpath:
logging.error(T('Cannot change permissions of %s'), clip_path(path))
logging.info("Traceback: ", exc_info=True)
def set_permissions(path, recursive=True):
""" Give folder tree and its files their proper permissions """
if not sabnzbd.WIN32:
umask = cfg.umask()
try:
# Make sure that user R+W+X is on
umask = int(umask, 8) | int('0700', 8)
report = True
except ValueError:
# No or no valid permissions
# Use the effective permissions of the session
# Don't report errors (because the system might not support it)
umask = int('0777', 8) & (sabnzbd.ORG_UMASK ^ int('0777', 8))
report = False
# Remove X bits for files
umask_file = umask & int('7666', 8)
if os.path.isdir(path):
if recursive:
# Parse the dir/file tree and set permissions
for root, _dirs, files in os.walk(path):
set_chmod(root, umask, report)
for name in files:
set_chmod(os.path.join(root, name), umask_file, report)
else:
set_chmod(path, umask, report)
else:
set_chmod(path, umask_file, report)
def clip_path(path):
r""" Remove \\?\ or \\?\UNC\ prefix from Windows path """
if sabnzbd.WIN32 and path and '?' in path:
path = path.replace('\\\\?\\UNC\\', '\\\\', 1).replace('\\\\?\\', '', 1)
return path
def long_path(path):
""" For Windows, convert to long style path; others, return same path """
if sabnzbd.WIN32 and path and not path.startswith('\\\\?\\'):
if path.startswith('\\\\'):
# Special form for UNC paths
path = path.replace('\\\\', '\\\\?\\UNC\\', 1)
else:
# Normal form for local paths
path = '\\\\?\\' + path
return path
def fix_unix_encoding(folder):
""" Fix bad name encoding for Unix systems """
if not sabnzbd.WIN32 and not sabnzbd.DARWIN and gUTF:
for root, dirs, files in os.walk(folder.encode('utf-8')):
for name in files:
new_name = special_fixer(name).encode('utf-8')
if name != new_name:
try:
shutil.move(os.path.join(root, name), os.path.join(root, new_name))
except:
logging.info('Cannot correct name of %s', os.path.join(root, name))
def get_urlbase(url):
""" Return the base URL (like http://server.domain.com/) """
parsed_uri = urlparse(url)

7
sabnzbd/newsunpack.py

@ -32,9 +32,10 @@ from subprocess import Popen
import sabnzbd
from sabnzbd.encoding import ubtou, TRANS, UNTRANS, unicoder, platform_encode, deunicode
import sabnzbd.utils.rarfile as rarfile
from sabnzbd.misc import format_time_string, find_on_path, make_script_path, int_conv, \
real_path, globber, globber_full, get_all_passwords, renamer, clip_path, \
has_win_device, calc_age, long_path, remove_file
from sabnzbd.misc import format_time_string, find_on_path, int_conv, \
get_all_passwords, calc_age
from sabnzbd.filesystem import make_script_path, real_path, globber, globber_full, \
renamer, clip_path,has_win_device, long_path
from sabnzbd.tvsort import SeriesSorter
import sabnzbd.cfg as cfg
from sabnzbd.constants import Status

3
sabnzbd/newswrapper.py

@ -423,6 +423,9 @@ class NewsWrapper(object):
if combine_chunk[-5:] == b'\r\n.\r\n':
return (chunk_len, True, False)
# Still in middle of data, so continue!
return (chunk_len, False, False)
def soft_reset(self):
self.timeout = None
self.article = None

7
sabnzbd/notifier.py

@ -36,7 +36,8 @@ import sabnzbd
import sabnzbd.cfg
from sabnzbd.encoding import unicoder
from sabnzbd.constants import NOTIFY_KEYS
from sabnzbd.misc import split_host, make_script_path
from sabnzbd.misc import split_host
from sabnzbd.filesystem import make_script_path
from sabnzbd.newsunpack import external_script
from gntp.core import GNTPRegister
@ -269,9 +270,7 @@ def send_growl(title, msg, gtype, test=None):
_GROWL, error = register_growl(growl_server, growl_password)
if _GROWL:
_GROWL_REG = True
if isinstance(msg, str):
msg = msg.decode('utf-8')
elif not isinstance(msg, str):
if not isinstance(msg, str):
msg = str(msg)
logging.debug('Send to Growl: %s %s %s', gtype, title, msg)
try:

4
sabnzbd/nzbqueue.py

@ -26,8 +26,8 @@ import datetime
import sabnzbd
from sabnzbd.nzbstuff import NzbObject
from sabnzbd.misc import exit_sab, cat_to_opts, remove_file, \
get_admin_path, remove_all, globber_full, int_conv, caller_name
from sabnzbd.misc import exit_sab, cat_to_opts, int_conv
from sabnzbd.filesystem import get_admin_path, remove_all, globber_full
from sabnzbd.panic import panic_queue
import sabnzbd.database as database
from sabnzbd.decorators import NzbQueueLocker

11
sabnzbd/nzbstuff.py

@ -38,11 +38,12 @@ from sabnzbd.constants import GIGI, ATTRIB_FILE, JOB_ADMIN, \
REPAIR_PRIORITY, TOP_PRIORITY, HIGH_PRIORITY, NORMAL_PRIORITY, \
LOW_PRIORITY, DEFAULT_PRIORITY, PAUSED_PRIORITY, DUP_PRIORITY, STOP_PRIORITY, \
RENAMES_FILE, MAX_BAD_ARTICLES, Status, PNFO
from sabnzbd.misc import to_units, cat_to_opts, cat_convert, sanitize_foldername, \
get_unique_path, get_admin_path, remove_all, sanitize_filename, globber_full, \
int_conv, set_permissions, format_time_string, long_path, trim_win_path, \
fix_unix_encoding, calc_age, is_obfuscated_filename, get_ext, get_filename, \
get_unique_filename, renamer, remove_file, remove_dir
from sabnzbd.misc import to_units, cat_to_opts, cat_convert, int_conv, \
format_time_string, calc_age
from sabnzbd.filesystem import sanitize_foldername, get_unique_path, get_admin_path, \
remove_all, sanitize_filename, globber_full, set_permissions, long_path, \
trim_win_path, fix_unix_encoding, is_obfuscated_filename, get_ext, get_filename, \
get_unique_filename, renamer
from sabnzbd.decorators import synchronized
import sabnzbd.config as config
import sabnzbd.cfg as cfg

2
sabnzbd/osxmenu.py

@ -37,7 +37,7 @@ import sabnzbd
import sabnzbd.cfg
from sabnzbd.constants import VALID_ARCHIVES, MEBI, Status
from sabnzbd.misc import get_filename, get_ext, diskspace, to_units
from sabnzbd.filesystem import get_filename, get_ext, diskspace, to_units
from sabnzbd.panic import launch_a_browser
import sabnzbd.notifier as notifier

10
sabnzbd/postproc.py

@ -30,11 +30,11 @@ import re
from sabnzbd.newsunpack import unpack_magic, par2_repair, external_processing, \
sfv_check, build_filelists, rar_sort
from threading import Thread
from sabnzbd.misc import real_path, get_unique_path, create_dirs, move_to_path, \
make_script_path, long_path, clip_path, \
on_cleanup_list, renamer, remove_dir, remove_all, globber, globber_full, \
set_permissions, cleanup_empty_directories, fix_unix_encoding, \
sanitize_and_trim_path, sanitize_files_in_folder, remove_file
from sabnzbd.misc import on_cleanup_list
from sabnzbd.filesystem import real_path, get_unique_path, create_dirs, move_to_path, \
make_script_path, long_path, clip_path, renamer, remove_dir, remove_all, globber, \
globber_full, set_permissions, cleanup_empty_directories, fix_unix_encoding, \
sanitize_and_trim_path, sanitize_files_in_folder
from sabnzbd.tvsort import Sorter
from sabnzbd.constants import REPAIR_PRIORITY, TOP_PRIORITY, POSTPROC_QUEUE_FILE_NAME, \
POSTPROC_QUEUE_VERSION, sample_match, JOB_ADMIN, Status, VERIFIED_FILE

2
sabnzbd/tvsort.py

@ -27,7 +27,7 @@ import logging
import re
import sabnzbd
from sabnzbd.misc import move_to_path, cleanup_empty_directories, get_unique_path, \
from sabnzbd.filesystem import move_to_path, cleanup_empty_directories, get_unique_path, \
get_unique_filename, get_ext, renamer, sanitize_foldername, clip_path
from sabnzbd.constants import series_match, date_match, year_match, sample_match
from sabnzbd.encoding import unicoder

9
sabnzbd/urlgrabber.py

@ -33,6 +33,7 @@ import sabnzbd
from sabnzbd.constants import FUTURE_Q_FOLDER, Status
from sabnzbd.encoding import unicoder
import sabnzbd.misc as misc
import sabnzbd.filesystem
import sabnzbd.dirscanner as dirscanner
from sabnzbd.nzbqueue import NzbQueue
import sabnzbd.cfg as cfg
@ -223,11 +224,11 @@ class URLGrabber(Thread):
continue
fn.close()
if '<nzb' in data and misc.get_ext(filename) != '.nzb':
if '<nzb' in data and sabnzbd.filesystem.get_ext(filename) != '.nzb':
filename += '.nzb'
# Sanitize filename first (also removing forbidden Windows-names)
filename = misc.sanitize_filename(filename)
filename = sabnzbd.filesystem.sanitize_filename(filename)
# Write data to temp file
path = os.path.join(cfg.admin_dir.get_path(), FUTURE_Q_FOLDER)
@ -238,7 +239,7 @@ class URLGrabber(Thread):
del data
# Check if nzb file
if misc.get_ext(filename) in ('.nzb', '.gz', 'bz2'):
if sabnzbd.filesystem.get_ext(filename) in ('.nzb', '.gz', 'bz2'):
res = dirscanner.ProcessSingleFile(filename, path, pp=pp, script=script, cat=cat, priority=priority,
nzbname=nzbname, nzo_info=nzo_info, url=future_nzo.url, keep=False,
nzo_id=future_nzo.nzo_id)[0]
@ -258,7 +259,7 @@ class URLGrabber(Thread):
else:
status, zf, exp_ext = dirscanner.is_archive(path)
if status == 0:
if misc.get_ext(filename) not in ('.rar', '.zip', '.7z'):
if sabnzbd.filesystem.get_ext(filename) not in ('.rar', '.zip', '.7z'):
filename = filename + exp_ext
os.rename(path, path + exp_ext)
path = path + exp_ext

8
sabnzbd/utils/pathbrowser.py

@ -71,7 +71,7 @@ def folders_at_path(path, include_parent = False, show_hidden = False):
path = '/'
# walk up the tree until we find a valid path
path = sabnzbd.misc.real_path(sabnzbd.DIR_HOME, path)
path = sabnzbd.filesystem.real_path(sabnzbd.DIR_HOME, path)
while path and not os.path.isdir(path):
if path == os.path.dirname(path):
return folders_at_path('', include_parent)
@ -101,16 +101,16 @@ def folders_at_path(path, include_parent = False, show_hidden = False):
except:
list_folder = False
if list_folder:
file_list.append({ 'name': sabnzbd.misc.clip_path(filename), 'path': sabnzbd.misc.clip_path(fpath) })
file_list.append({ 'name': sabnzbd.filesystem.clip_path(filename), 'path': sabnzbd.filesystem.clip_path(fpath) })
# Remove junk and sort results
file_list = [entry for entry in file_list if os.path.isdir(entry['path']) and entry['name'].lower() not in _JUNKFOLDERS]
file_list = sorted(file_list, key=lambda x: os.path.basename(x['name']).lower())
# Add current path
file_list.insert(0, {'current_path': sabnzbd.misc.clip_path(path)})
file_list.insert(0, {'current_path': sabnzbd.filesystem.clip_path(path)})
if include_parent and parent_path != path:
file_list.insert(1,{ 'name': "..", 'path': sabnzbd.misc.clip_path(parent_path) })
file_list.insert(1,{ 'name': "..", 'path': sabnzbd.filesystem.clip_path(parent_path) })
return file_list

2
sabnzbd/utils/rarfile.py

@ -824,7 +824,7 @@ class RarFile(object):
"""
# Modified for SABnzbd by clipping paths
# and de-unicoding only here
from sabnzbd.misc import clip_path
from sabnzbd.filesystem import clip_path
from sabnzbd.encoding import deunicode
rarpath = deunicode(clip_path(self._rarfile))

2
sabnzbd/utils/upload.py

@ -24,7 +24,7 @@ import logging
import os
from sabnzbd.encoding import unicoder
import sabnzbd.cfg as cfg
from sabnzbd.misc import get_ext, get_filename, get_from_url
from sabnzbd.filesystem import get_ext, get_filename
import sabnzbd.newsunpack
from sabnzbd.constants import VALID_ARCHIVES

Loading…
Cancel
Save