Browse Source

Merge branch 'maxkoryukov-fix/opt-directories' into develop

pull/6004/merge
Ruud 9 years ago
parent
commit
f3a13e6283
  1. 3
      .travis.yml
  2. 2
      Gruntfile.js
  3. 3
      couchpotato/core/_base/_core.py
  4. 5
      couchpotato/core/loader.py
  5. 35
      couchpotato/core/plugins/browser.py
  6. 39
      couchpotato/core/plugins/browser_test.py
  7. 3
      couchpotato/core/plugins/manage.py
  8. 141
      couchpotato/core/settings.py
  9. 122
      couchpotato/core/softchroot.py
  10. 121
      couchpotato/core/softchroot_test.py
  11. 54
      couchpotato/core/test_softchroot.py
  12. 2
      couchpotato/environment.py
  13. 19
      couchpotato/environment_test.py
  14. 16
      couchpotato/runner.py
  15. 7
      couchpotato/static/scripts/combined.base.min.js
  16. 13
      couchpotato/static/scripts/page/settings.js
  17. 12
      couchpotato/static/style/combined.min.css
  18. 5
      py-dev-requirements.txt

3
.travis.yml

@ -14,6 +14,7 @@ cache:
pip: true pip: true
directories: directories:
- node_modules - node_modules
- libs
# command to install dependencies # command to install dependencies
install: install:
@ -25,6 +26,8 @@ install:
- pip install --upgrade nose - pip install --upgrade nose
# - pip install rednose # - pip install rednose
- nosetests --plugins
# command to run tests # command to run tests
script: script:
- grunt test - grunt test

2
Gruntfile.js

@ -201,7 +201,7 @@ module.exports = function(grunt){
config: './.nosetestsrc', config: './.nosetestsrc',
// 'rednose' is a colored output for nose test-runner. But we do not requre colors on travis-ci // 'rednose' is a colored output for nose test-runner. But we do not requre colors on travis-ci
rednose: config.colorful_tests_output, rednose: config.colorful_tests_output,
externalNose: true externalNose: true,
}, },
test: { test: {

3
couchpotato/core/_base/_core.py

@ -249,6 +249,7 @@ config = [{
{ {
'name': 'username', 'name': 'username',
'default': '', 'default': '',
'ui-meta' : 'rw',
}, },
{ {
'name': 'password', 'name': 'password',
@ -302,7 +303,7 @@ config = [{
{ {
'name': 'api_key', 'name': 'api_key',
'default': uuid4().hex, 'default': uuid4().hex,
'readonly': 1, 'ui-meta' : 'ro',
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>', 'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
}, },
{ {

5
couchpotato/core/loader.py

@ -99,6 +99,11 @@ class Loader(object):
path = os.path.join(dir_name, name) path = os.path.join(dir_name, name)
ext = os.path.splitext(path)[1] ext = os.path.splitext(path)[1]
ext_length = len(ext) ext_length = len(ext)
# SKIP test files:
if path.endswith('_test.py'):
continue
if name != 'static' and ((os.path.isdir(path) and os.path.isfile(os.path.join(path, '__init__.py'))) if name != 'static' and ((os.path.isdir(path) and os.path.isfile(os.path.join(path, '__init__.py')))
or (os.path.isfile(path) and ext == '.py')): or (os.path.isfile(path) and ext == '.py')):
name = name[:-ext_length] if ext_length > 0 else name name = name[:-ext_length] if ext_length > 0 else name

35
couchpotato/core/plugins/browser.py

@ -10,10 +10,10 @@ from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import sp, ss, toUnicode from couchpotato.core.helpers.encoding import sp, ss, toUnicode
from couchpotato.core.helpers.variable import getUserDir from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.softchroot import SoftChroot
log = CPLog(__name__) from couchpotato.environment import Env
log = CPLog(__name__)
if os.name == 'nt': if os.name == 'nt':
import imp import imp
@ -29,13 +29,9 @@ if os.name == 'nt':
autoload = 'FileBrowser' autoload = 'FileBrowser'
class FileBrowser(Plugin): class FileBrowser(Plugin):
def __init__(self): def __init__(self):
soft_chroot_dir = self.conf('soft_chroot', section='core')
self.soft_chroot = SoftChroot(soft_chroot_dir)
addApiView('directory.list', self.view, docs = { addApiView('directory.list', self.view, docs = {
'desc': 'Return the directory list of a given directory', 'desc': 'Return the directory list of a given directory',
'params': { 'params': {
@ -81,17 +77,20 @@ class FileBrowser(Plugin):
return driveletters return driveletters
def view(self, path = '/', show_hidden = True, **kwargs): def view(self, path = '/', show_hidden = True, **kwargs):
soft_chroot = Env.get('softchroot')
home = getUserDir() home = getUserDir()
if self.soft_chroot.enabled: if soft_chroot.enabled:
if not self.soft_chroot.is_subdir(home): if not soft_chroot.is_subdir(home):
home = self.soft_chroot.chdir home = soft_chroot.get_chroot()
if not path: if not path:
path = home path = home
if path.endswith(os.path.sep): if path.endswith(os.path.sep):
path = path.rstrip(os.path.sep) path = path.rstrip(os.path.sep)
elif self.soft_chroot.enabled: else:
path = self.soft_chroot.add(path) path = soft_chroot.chroot2abs(path)
try: try:
dirs = self.getDirectories(path = path, show_hidden = show_hidden) dirs = self.getDirectories(path = path, show_hidden = show_hidden)
@ -99,8 +98,8 @@ class FileBrowser(Plugin):
log.error('Failed getting directory "%s" : %s', (path, traceback.format_exc())) log.error('Failed getting directory "%s" : %s', (path, traceback.format_exc()))
dirs = [] dirs = []
if self.soft_chroot.enabled: if soft_chroot.enabled:
dirs = map(self.soft_chroot.cut, dirs) dirs = map(soft_chroot.abs2chroot, dirs)
parent = os.path.dirname(path.rstrip(os.path.sep)) parent = os.path.dirname(path.rstrip(os.path.sep))
if parent == path.rstrip(os.path.sep): if parent == path.rstrip(os.path.sep):
@ -111,16 +110,16 @@ class FileBrowser(Plugin):
# TODO : check on windows: # TODO : check on windows:
is_root = path == '/' is_root = path == '/'
if self.soft_chroot.enabled: if soft_chroot.enabled:
is_root = self.soft_chroot.is_root_abs(path) is_root = soft_chroot.is_root_abs(path)
# fix paths: # fix paths:
if self.soft_chroot.is_subdir(parent): if soft_chroot.is_subdir(parent):
parent = self.soft_chroot.cut(parent) parent = soft_chroot.abs2chroot(parent)
else: else:
parent = os.path.sep parent = os.path.sep
home = self.soft_chroot.cut(home) home = soft_chroot.abs2chroot(home)
return { return {
'is_root': is_root, 'is_root': is_root,

39
couchpotato/core/plugins/test_browser.py → couchpotato/core/plugins/browser_test.py

@ -1,43 +1,46 @@
import sys #import sys
import os import os
import logging
import mock
import unittest import unittest
from unittest import TestCase from unittest import TestCase
#from mock import MagicMock
from couchpotato.core.plugins.browser import FileBrowser from couchpotato.core.plugins.browser import FileBrowser
from couchpotato.core.softchroot import SoftChroot from couchpotato.core.softchroot import SoftChroot
CHROOT_DIR = '/tmp/' CHROOT_DIR = '/tmp/'
# 'couchpotato.core.plugins.browser.Env',
@mock.patch('couchpotato.core.plugins.browser.Env', name='EnvMock')
class FileBrowserChrootedTest(TestCase): class FileBrowserChrootedTest(TestCase):
def setUp(self): def setUp(self):
self.b = FileBrowser() self.b = FileBrowser()
# TODO : remove scrutch: def tuneMock(self, env):
self.b.soft_chroot = SoftChroot(CHROOT_DIR) #set up mock:
sc = SoftChroot()
# Logger sc.initialize(CHROOT_DIR)
logger = logging.getLogger() env.get.return_value = sc
logger.setLevel(logging.DEBUG)
# To screen
hdlr = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S')
hdlr.setFormatter(formatter)
#logger.addHandler(hdlr)
def test_soft_chroot_enabled(self):
self.assertTrue( self.b.soft_chroot.enabled)
def test_view__chrooted_path_none(self): def test_view__chrooted_path_none(self, env):
#def view(self, path = '/', show_hidden = True, **kwargs): #def view(self, path = '/', show_hidden = True, **kwargs):
self.tuneMock(env)
r = self.b.view(None) r = self.b.view(None)
self.assertEqual(r['home'], '/') self.assertEqual(r['home'], '/')
self.assertEqual(r['parent'], '/') self.assertEqual(r['parent'], '/')
self.assertTrue(r['is_root']) self.assertTrue(r['is_root'])
def test_view__chrooted_path_chroot(self): def test_view__chrooted_path_chroot(self, env):
#def view(self, path = '/', show_hidden = True, **kwargs): #def view(self, path = '/', show_hidden = True, **kwargs):
self.tuneMock(env)
for path, parent in [('/asdf','/'), (CHROOT_DIR, '/'), ('/mnk/123/t', '/mnk/123/')]: for path, parent in [('/asdf','/'), (CHROOT_DIR, '/'), ('/mnk/123/t', '/mnk/123/')]:
r = self.b.view(path) r = self.b.view(path)
path_strip = path path_strip = path

3
couchpotato/core/plugins/manage.py

@ -251,8 +251,7 @@ class Manage(Plugin):
def directories(self): def directories(self):
try: try:
if self.conf('library', default = '').strip(): return self.conf('library', default = [])
return splitString(self.conf('library', default = ''), '::')
except: except:
pass pass

141
couchpotato/core/settings.py

@ -7,7 +7,6 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat
from couchpotato.core.softchroot import SoftChroot
class Settings(object): class Settings(object):
@ -58,6 +57,7 @@ class Settings(object):
self.file = None self.file = None
self.p = None self.p = None
self.log = None self.log = None
self.directories_delimiter = "::"
def setFile(self, config_file): def setFile(self, config_file):
self.file = config_file self.file = config_file
@ -93,6 +93,17 @@ class Settings(object):
for option_name, option in options.items(): for option_name, option in options.items():
self.setDefault(section_name, option_name, option.get('default', '')) self.setDefault(section_name, option_name, option.get('default', ''))
# Set UI-meta for option (hidden/ro/rw)
if option.get('ui-meta'):
value = option.get('ui-meta')
if value:
value = value.lower()
if value in ['hidden', 'rw', 'ro']:
meta_option_name = option_name + self.optionMetaSuffix()
self.setDefault(section_name, meta_option_name, value)
else:
self.log.warning('Wrong value for option %s.%s : ui-meta can not be equal to "%s"', (section_name, option_name, value))
# Migrate old settings from old location to the new location # Migrate old settings from old location to the new location
if option.get('migrate_from'): if option.get('migrate_from'):
if self.p.has_option(option.get('migrate_from'), option_name): if self.p.has_option(option.get('migrate_from'), option_name):
@ -114,7 +125,6 @@ class Settings(object):
self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option)) self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option))
return None return None
return self.p.set(section, option, value) return self.p.set(section, option, value)
def get(self, option = '', section = 'core', default = None, type = None): def get(self, option = '', section = 'core', default = None, type = None):
@ -123,9 +133,7 @@ class Settings(object):
return None return None
try: try:
type = self.getType(section, option)
try: type = self.types[section][option]
except: type = 'unicode' if not type else type
if hasattr(self, 'get%s' % type.capitalize()): if hasattr(self, 'get%s' % type.capitalize()):
return getattr(self, 'get%s' % type.capitalize())(section, option) return getattr(self, 'get%s' % type.capitalize())(section, option)
@ -168,12 +176,21 @@ class Settings(object):
except: except:
return tryFloat(self.p.get(section, option)) return tryFloat(self.p.get(section, option))
def getDirectories(self, section, option):
value = self.p.get(section, option)
if value:
return map(str.strip, str.split(value, self.directories_delimiter))
return []
def getUnicode(self, section, option): def getUnicode(self, section, option):
value = self.p.get(section, option).decode('unicode_escape') value = self.p.get(section, option).decode('unicode_escape')
return toUnicode(value).strip() return toUnicode(value).strip()
def getValues(self): def getValues(self):
from couchpotato.environment import Env
values = {} values = {}
soft_chroot = Env.get('softchroot')
# TODO : There is two commented "continue" blocks (# COMMENTED_SKIPPING). They both are good... # TODO : There is two commented "continue" blocks (# COMMENTED_SKIPPING). They both are good...
# ... but, they omit output of values of hidden and non-readable options # ... but, they omit output of values of hidden and non-readable options
@ -198,13 +215,24 @@ class Settings(object):
#if not self.isOptionReadable(section, option_name): #if not self.isOptionReadable(section, option_name):
# continue # continue
is_password = False value = self.get(option_name, section)
try: is_password = self.types[section][option_name] == 'password'
except: pass is_password = self.getType(section, option_name) == 'password'
if is_password and value:
value = len(value) * '*'
values[section][option_name] = self.get(option_name, section) # chrootify directory before sending to UI:
if is_password and values[section][option_name]: if (self.getType(section, option_name) == 'directory') and value:
values[section][option_name] = len(values[section][option_name]) * '*' try: value = soft_chroot.abs2chroot(value)
except: value = ""
# chrootify directories before sending to UI:
if (self.getType(section, option_name) == 'directories'):
if (not value):
value = []
try : value = map(soft_chroot.abs2chroot, value)
except : value = []
values[section][option_name] = value
return values return values
@ -212,8 +240,6 @@ class Settings(object):
with open(self.file, 'wb') as configfile: with open(self.file, 'wb') as configfile:
self.p.write(configfile) self.p.write(configfile)
self.log.debug('Saved settings')
def addSection(self, section): def addSection(self, section):
if not self.p.has_section(section): if not self.p.has_section(section):
self.p.add_section(section) self.p.add_section(section)
@ -228,6 +254,12 @@ class Settings(object):
self.types[section][option] = type self.types[section][option] = type
def getType(self, section, option):
type = None
try: type = self.types[section][option]
except: type = 'unicode' if not type else type
return type
def addOptions(self, section_name, options): def addOptions(self, section_name, options):
# no additional actions (related to ro-rw options) are required here # no additional actions (related to ro-rw options) are required here
if not self.options.get(section_name): if not self.options.get(section_name):
@ -243,36 +275,42 @@ class Settings(object):
res = {} res = {}
if isinstance(self.options, dict): # it is required to filter invisible options for UI, but also we should
for section_key in self.options.keys(): # preserve original tree for server's purposes.
section = self.options[section_key] # So, next loops do one thing: copy options to res and in the process
section_name = section.get('name') if 'name' in section else section_key # 1. omit NON-READABLE (for UI) options, and
if self.isSectionReadable(section_name) and isinstance(section, dict): # 2. put flags on READONLY options
s = {} for section_key in self.options.keys():
sg = [] section_orig = self.options[section_key]
for section_field in section: section_name = section_orig.get('name') if 'name' in section_orig else section_key
if section_field.lower() != 'groups': if self.isSectionReadable(section_name):
s[section_field] = section[section_field] section_copy = {}
else: section_copy_groups = []
groups = section['groups'] for section_field in section_orig:
for group in groups: if section_field.lower() != 'groups':
g = {} section_copy[section_field] = section_orig[section_field]
go = [] else:
for group_field in group: for group_orig in section_orig['groups']:
if group_field.lower() != 'options': group_copy = {}
g[group_field] = group[group_field] group_copy_options = []
else: for group_field in group_orig:
for option in group[group_field]: if group_field.lower() != 'options':
option_name = option.get('name') group_copy[group_field] = group_orig[group_field]
if self.isOptionReadable(section_name, option_name): else:
go.append(option) for option in group_orig[group_field]:
option['writable'] = self.isOptionWritable(section_name, option_name) option_name = option.get('name')
if len(go)>0: # You should keep in mind, that READONLY = !IS_WRITABLE
g['options'] = go # and IS_READABLE is a different thing
sg.append(g) if self.isOptionReadable(section_name, option_name):
if len(sg)>0: group_copy_options.append(option)
s['groups'] = sg if not self.isOptionWritable(section_name, option_name):
res[section_key] = s option['readonly'] = True
if len(group_copy_options)>0:
group_copy['options'] = group_copy_options
section_copy_groups.append(group_copy)
if len(section_copy_groups)>0:
section_copy['groups'] = section_copy_groups
res[section_key] = section_copy
return res return res
@ -288,10 +326,19 @@ class Settings(object):
option = kwargs.get('name') option = kwargs.get('name')
value = kwargs.get('value') value = kwargs.get('value')
if (section in self.types) and (option in self.types[section]) and (self.types[section][option] == 'directory'): from couchpotato.environment import Env
soft_chroot_dir = self.get('soft_chroot', default = None) soft_chroot = Env.get('softchroot')
soft_chroot = SoftChroot(soft_chroot_dir)
value = soft_chroot.add(str(value)) if self.getType(section, option) == 'directory':
value = soft_chroot.chroot2abs(value)
if self.getType(section, option) == 'directories':
import json
value = json.loads(value)
if not (value and isinstance(value, list)):
value = []
value = map(soft_chroot.chroot2abs, value)
value = self.directories_delimiter.join(value)
# See if a value handler is attached, use that as value # See if a value handler is attached, use that as value
new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True) new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True)

122
couchpotato/core/softchroot.py

@ -2,39 +2,99 @@ import os
import sys import sys
class SoftChrootInitError(IOError):
"""Error during soft-chroot initialization"""
pass
class SoftChroot: class SoftChroot:
def __init__(self, chdir): """Soft Chroot module
self.enabled = False
Provides chroot feature for interation with Web-UI. Since it is not real chroot, so the name is SOFT CHROOT.
The module prevents access to entire file-system, allowing access only to subdirs of SOFT-CHROOT directory.
"""
def __init__(self):
self.enabled = None
self.chdir = None
def initialize(self, chdir):
""" initialize module, by setting soft-chroot-directory
Sets soft-chroot directory and 'enabled'-flag
Args:
self (SoftChroot) : self
chdir (string) : absolute path to soft-chroot
Raises:
SoftChrootInitError: when chdir doesn't exist
"""
orig_chdir = chdir
if chdir:
chdir = chdir.strip()
self.chdir = chdir if (chdir):
# enabling soft-chroot:
if not os.path.isdir(chdir):
raise SoftChrootInitError(2, 'SOFT-CHROOT is requested, but the folder doesn\'t exist', orig_chdir)
if None != self.chdir:
self.chdir = self.chdir.strip()
self.chdir = self.chdir.rstrip(os.path.sep) + os.path.sep
self.enabled = True self.enabled = True
self.chdir = chdir.rstrip(os.path.sep) + os.path.sep
else:
self.enabled = False
def get_chroot(self):
"""Returns root in chrooted environment
Raises:
RuntimeError: when `SoftChroot` is not initialized OR enabled
"""
if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if not self.enabled:
raise RuntimeError('SoftChroot is not enabled')
return self.chdir
def is_root_abs(self, abspath): def is_root_abs(self, abspath):
if not self.enabled: """ Checks whether absolute path @abspath is the root in the soft-chrooted environment"""
raise Exception('chroot disabled') if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if None == abspath: if None == abspath:
return False raise ValueError('abspath can not be None')
if not self.enabled:
# if not chroot environment : check, whether parent is the same dir:
parent = os.path.dirname(abspath.rstrip(os.path.sep))
return parent==abspath
# in soft-chrooted env: check, that path == chroot
path = abspath.rstrip(os.path.sep) + os.path.sep path = abspath.rstrip(os.path.sep) + os.path.sep
return self.chdir == path return self.chdir == path
def is_subdir(self, path): def is_subdir(self, abspath):
""" Checks whether @abspath is subdir (on any level) of soft-chroot"""
if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if None == abspath:
return False
if not self.enabled: if not self.enabled:
return True return True
if None == path: if not abspath.endswith(os.path.sep):
return False abspath += os.path.sep
if not path.endswith(os.path.sep): return abspath.startswith(self.chdir)
path += os.path.sep
return path.startswith(self.chdir) def chroot2abs(self, path):
""" Converts chrooted path to absolute path"""
def add(self, path): if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if not self.enabled: if not self.enabled:
return path return path
@ -42,23 +102,33 @@ class SoftChroot:
return self.chdir return self.chdir
if not path.startswith(os.path.sep): if not path.startswith(os.path.sep):
raise ValueError("path must starts with '/'") path = os.path.sep + path
return self.chdir[:-1] + path return self.chdir[:-1] + path
def cut(self, path): def abs2chroot(self, path, force = False):
if not self.enabled: """ Converts absolute path to chrooted path"""
return path
if None == path or 0==len(path): if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if None == path:
raise ValueError('path is empty') raise ValueError('path is empty')
if not self.enabled:
return path
if path == self.chdir.rstrip(os.path.sep): if path == self.chdir.rstrip(os.path.sep):
return '/' return '/'
resulst = None
if not path.startswith(self.chdir): if not path.startswith(self.chdir):
raise ValueError("path must starts with 'chdir'") if (force):
result = self.get_chroot()
l = len(self.chdir)-1 else:
raise ValueError("path must starts with 'chdir': %s" % path)
return path[l:] else:
l = len(self.chdir)-1
result = path[l:]
return result

121
couchpotato/core/softchroot_test.py

@ -0,0 +1,121 @@
import sys
import os
import logging
import unittest
from unittest import TestCase
#from mock import MagicMock
from couchpotato.core.softchroot import SoftChroot
CHROOT_DIR = '/tmp/'
class SoftChrootNonInitialized(TestCase):
def setUp(self):
self.b = SoftChroot()
def test_is_root_abs(self):
with self.assertRaises(RuntimeError):
self.b.is_root_abs('1')
def test_is_subdir(self):
with self.assertRaises(RuntimeError):
self.b.is_subdir('1')
def test_chroot2abs(self):
with self.assertRaises(RuntimeError):
self.b.chroot2abs('1')
def test_abs2chroot(self):
with self.assertRaises(RuntimeError):
self.b.abs2chroot('1')
def test_get_root(self):
with self.assertRaises(RuntimeError):
self.b.get_chroot()
class SoftChrootNOTEnabledTest(TestCase):
def setUp(self):
self.b = SoftChroot()
self.b.initialize(None)
def test_get_root(self):
with self.assertRaises(RuntimeError):
self.b.get_chroot()
def test_chroot2abs_noleading_slash(self):
path = 'no_leading_slash'
self.assertEqual( self.b.chroot2abs(path), path )
def test_chroot2abs(self):
self.assertIsNone( self.b.chroot2abs(None), None )
self.assertEqual( self.b.chroot2abs(''), '' )
self.assertEqual( self.b.chroot2abs('/asdf'), '/asdf' )
def test_abs2chroot_raise_on_empty(self):
with self.assertRaises(ValueError):
self.b.abs2chroot(None)
def test_abs2chroot(self):
self.assertEqual( self.b.abs2chroot(''), '' )
self.assertEqual( self.b.abs2chroot('/asdf'), '/asdf' )
self.assertEqual( self.b.abs2chroot('/'), '/' )
def test_get_root(self):
with self.assertRaises(RuntimeError):
self.b.get_chroot()
class SoftChrootEnabledTest(TestCase):
def setUp(self):
self.b = SoftChroot()
self.b.initialize(CHROOT_DIR)
def test_enabled(self):
self.assertTrue( self.b.enabled)
def test_is_subdir(self):
self.assertFalse( self.b.is_subdir('') )
self.assertFalse( self.b.is_subdir(None) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_subdir(noslash) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR + 'come') )
def test_is_root_abs_none(self):
with self.assertRaises(ValueError):
self.assertFalse( self.b.is_root_abs(None) )
def test_is_root_abs(self):
self.assertFalse( self.b.is_root_abs('') )
self.assertTrue( self.b.is_root_abs(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_root_abs(noslash) )
self.assertFalse( self.b.is_root_abs(CHROOT_DIR + 'come') )
def test_chroot2abs_noleading_slash(self):
path = 'no_leading_slash'
path_sl = CHROOT_DIR + path
#with self.assertRaises(ValueError):
# self.b.chroot2abs('no_leading_slash')
self.assertEqual( self.b.chroot2abs(path), path_sl )
def test_chroot2abs(self):
self.assertEqual( self.b.chroot2abs(None), CHROOT_DIR )
self.assertEqual( self.b.chroot2abs(''), CHROOT_DIR )
self.assertEqual( self.b.chroot2abs('/asdf'), CHROOT_DIR + 'asdf' )
def test_abs2chroot_raise_on_empty(self):
with self.assertRaises(ValueError): self.b.abs2chroot(None)
with self.assertRaises(ValueError): self.b.abs2chroot('')
def test_abs2chroot(self):
self.assertEqual( self.b.abs2chroot(CHROOT_DIR + 'asdf'), '/asdf' )
self.assertEqual( self.b.abs2chroot(CHROOT_DIR), '/' )
self.assertEqual( self.b.abs2chroot(CHROOT_DIR.rstrip(os.path.sep)), '/' )
def test_get_root(self):
self.assertEqual( self.b.get_chroot(), CHROOT_DIR )

54
couchpotato/core/test_softchroot.py

@ -1,54 +0,0 @@
import sys
import os
import logging
import unittest
from unittest import TestCase
#from mock import MagicMock
from couchpotato.core.softchroot import SoftChroot
CHROOT_DIR = '/tmp/'
class SoftChrootEnabledTest(TestCase):
def setUp(self):
self.b = SoftChroot(CHROOT_DIR)
def test_enabled(self):
self.assertTrue( self.b.enabled)
def test_is_subdir(self):
self.assertFalse( self.b.is_subdir('') )
self.assertFalse( self.b.is_subdir(None) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_subdir(noslash) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR + 'come') )
def test_is_root_abs(self):
self.assertFalse( self.b.is_root_abs('') )
self.assertFalse( self.b.is_root_abs(None) )
self.assertTrue( self.b.is_root_abs(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_root_abs(noslash) )
self.assertFalse( self.b.is_root_abs(CHROOT_DIR + 'come') )
def test_add(self):
with self.assertRaises(ValueError):
self.b.add('no_leading_slash')
self.assertEqual( self.b.add(None), CHROOT_DIR )
self.assertEqual( self.b.add(''), CHROOT_DIR )
self.assertEqual( self.b.add('/asdf'), CHROOT_DIR + 'asdf' )
def test_cut(self):
with self.assertRaises(ValueError): self.b.cut(None)
with self.assertRaises(ValueError): self.b.cut('')
self.assertEqual( self.b.cut(CHROOT_DIR + 'asdf'), '/asdf' )
self.assertEqual( self.b.cut(CHROOT_DIR), '/' )
self.assertEqual( self.b.cut(CHROOT_DIR.rstrip(os.path.sep)), '/' )

2
couchpotato/environment.py

@ -5,6 +5,7 @@ from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.loader import Loader from couchpotato.core.loader import Loader
from couchpotato.core.settings import Settings from couchpotato.core.settings import Settings
from couchpotato.core.softchroot import SoftChroot
class Env(object): class Env(object):
@ -19,6 +20,7 @@ class Env(object):
_settings = Settings() _settings = Settings()
_database = Database() _database = Database()
_loader = Loader() _loader = Loader()
_softchroot = SoftChroot()
_cache = None _cache = None
_options = None _options = None
_args = None _args = None

19
couchpotato/environment_test.py

@ -0,0 +1,19 @@
import unittest
from unittest import TestCase
import mock
from couchpotato.environment import Env
class EnvironmentBaseTest(TestCase):
def test_appname(self):
self.assertEqual('CouchPotato', Env.get('appname'))
def test_set_get_appname(self):
x = 'NEWVALUE'
Env.set('appname', x)
self.assertEqual(x, Env.get('appname'))
def test_get_softchroot(self):
from couchpotato.core.softchroot import SoftChroot
sc = Env.get('softchroot')
self.assertIsInstance(sc, SoftChroot)

16
couchpotato/runner.py

@ -23,7 +23,7 @@ import requests
from requests.packages.urllib3 import disable_warnings from requests.packages.urllib3 import disable_warnings
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.web import Application, StaticFileHandler, RedirectHandler from tornado.web import Application, StaticFileHandler, RedirectHandler
from couchpotato.core.softchroot import SoftChrootInitError
def getOptions(args): def getOptions(args):
@ -218,13 +218,13 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Check soft-chroot dir exists: # Check soft-chroot dir exists:
try: try:
soft_chroot = Env.setting('soft_chroot', section = 'core', default = None, type='unicode' ) # Load Soft-Chroot
soft_chroot = Env.get('softchroot')
if (None != soft_chroot): soft_chroot_dir = Env.setting('soft_chroot', section = 'core', default = None, type='unicode' )
soft_chroot = soft_chroot.strip() soft_chroot.initialize(soft_chroot_dir)
if (len(soft_chroot)>0) and (not os.path.isdir(soft_chroot)): except SoftChrootInitError as exc:
log.error('SOFT-CHROOT is defined, but the folder doesn\'t exist') log.error(exc)
return return
except: except:
log.error('Unable to check whether SOFT-CHROOT is defined') log.error('Unable to check whether SOFT-CHROOT is defined')
return return

7
couchpotato/static/scripts/combined.base.min.js

@ -1342,7 +1342,7 @@ var OptionBase = new Class({
self.section = section; self.section = section;
self.name = name; self.name = name;
self.value = self.previous_value = value; self.value = self.previous_value = value;
self.read_only = !(options && options.writable); self.read_only = !(options && !options.readonly);
self.createBase(); self.createBase();
self.create(); self.create();
self.createHint(); self.createHint();
@ -1784,12 +1784,11 @@ Option.Directory = new Class({
Option.Directories = new Class({ Option.Directories = new Class({
Extends: Option.String, Extends: Option.String,
directories: [], directories: [],
delimiter: "::",
afterInject: function() { afterInject: function() {
var self = this; var self = this;
self.el.setStyle("display", "none"); self.el.setStyle("display", "none");
self.directories = []; self.directories = [];
self.getValue().split(self.delimiter).each(function(value) { self.getSettingValue().each(function(value) {
self.addDirectory(value); self.addDirectory(value);
}); });
self.addDirectory(); self.addDirectory();
@ -1833,7 +1832,7 @@ Option.Directories = new Class({
dirs.include(dir.getValue()); dirs.include(dir.getValue());
} else $(dir).addClass("is_empty"); } else $(dir).addClass("is_empty");
}); });
self.input.set("value", dirs.join(self.delimiter)); self.input.set("value", JSON.encode(dirs));
self.input.fireEvent("change"); self.input.fireEvent("change");
self.addDirectory(); self.addDirectory();
} }

13
couchpotato/static/scripts/page/settings.js

@ -343,7 +343,7 @@ var OptionBase = new Class({
self.section = section; self.section = section;
self.name = name; self.name = name;
self.value = self.previous_value = value; self.value = self.previous_value = value;
self.read_only = !(options && options.writable); self.read_only = !(options && !options.readonly);
self.createBase(); self.createBase();
self.create(); self.create();
@ -991,7 +991,6 @@ Option.Directories = new Class({
Extends: Option.String, Extends: Option.String,
directories: [], directories: [],
delimiter: '::',
afterInject: function(){ afterInject: function(){
var self = this; var self = this;
@ -999,9 +998,11 @@ Option.Directories = new Class({
self.el.setStyle('display', 'none'); self.el.setStyle('display', 'none');
self.directories = []; self.directories = [];
self.getValue().split(self.delimiter).each(function(value){
self.getSettingValue().each(function(value){
self.addDirectory(value); self.addDirectory(value);
}); });
self.addDirectory(); self.addDirectory();
}, },
@ -1025,7 +1026,7 @@ Option.Directories = new Class({
else else
$(dir).inject(dirs.getLast(), 'after'); $(dir).inject(dirs.getLast(), 'after');
// Replace some properties // TODO : Replace some properties
dir.save = self.saveItems.bind(self); dir.save = self.saveItems.bind(self);
$(dir).getElement('label').set('text', 'Movie Folder'); $(dir).getElement('label').set('text', 'Movie Folder');
$(dir).getElement('.formHint').destroy(); $(dir).getElement('.formHint').destroy();
@ -1068,14 +1069,12 @@ Option.Directories = new Class({
$(dir).addClass('is_empty'); $(dir).addClass('is_empty');
}); });
self.input.set('value', dirs.join(self.delimiter)); self.input.set('value', JSON.encode(dirs) );
self.input.fireEvent('change'); self.input.fireEvent('change');
self.addDirectory(); self.addDirectory();
} }
}); });
Option.Choice = new Class({ Option.Choice = new Class({

12
couchpotato/static/style/combined.min.css

@ -54,7 +54,7 @@
.search_form .results_container .results .media_result .options .add a{color:#FFF} .search_form .results_container .results .media_result .options .add a{color:#FFF}
.search_form .results_container .results .media_result .options .button{display:block;background:#ac0000;text-align:center;margin:0} .search_form .results_container .results .media_result .options .button{display:block;background:#ac0000;text-align:center;margin:0}
.dark .search_form .results_container .results .media_result .options .button{background:#f85c22} .dark .search_form .results_container .results .media_result .options .button{background:#f85c22}
.search_form .results_container .results .media_result .options .message{font-size:20px;color:#fff} .search_form .results_container .results .media_result .options .message{font-size:20px;color:#FFF}
.search_form .results_container .results .media_result .thumbnail{width:30px;min-height:100%;display:block;margin:0;vertical-align:top} .search_form .results_container .results .media_result .thumbnail{width:30px;min-height:100%;display:block;margin:0;vertical-align:top}
.search_form .results_container .results .media_result .data{position:absolute;height:100%;top:0;left:30px;right:0;cursor:pointer;border-top:1px solid rgba(255,255,255,.08);transition:all .4s cubic-bezier(.9,0,.1,1);will-change:transform;-webkit-transform:translateX(0) rotateZ(360deg);transform:translateX(0) rotateZ(360deg);background:#FFF} .search_form .results_container .results .media_result .data{position:absolute;height:100%;top:0;left:30px;right:0;cursor:pointer;border-top:1px solid rgba(255,255,255,.08);transition:all .4s cubic-bezier(.9,0,.1,1);will-change:transform;-webkit-transform:translateX(0) rotateZ(360deg);transform:translateX(0) rotateZ(360deg);background:#FFF}
.dark .search_form .results_container .results .media_result .data{background:#2d2d2d;border-color:rgba(255,255,255,.08)} .dark .search_form .results_container .results .media_result .data{background:#2d2d2d;border-color:rgba(255,255,255,.08)}
@ -95,17 +95,15 @@
.page.home .search_form .wrapper .input input{padding-right:44px;font-size:1em} .page.home .search_form .wrapper .input input{padding-right:44px;font-size:1em}
.page.home .search_form .wrapper .results_container{top:44px;min-height:44px} .page.home .search_form .wrapper .results_container{top:44px;min-height:44px}
} }
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results{max-height:400px} @media (min-width:480px){.page.home .search_form .wrapper .results_container .results .media_result .data,.page.home .search_form .wrapper .results_container .results .media_result .options{left:40px}
.page.home .search_form .wrapper .results_container .results{max-height:400px}
.page.home .search_form .wrapper .results_container .results .media_result{height:66px} .page.home .search_form .wrapper .results_container .results .media_result{height:66px}
.page.home .search_form .wrapper .results_container .results .media_result .thumbnail{width:40px} .page.home .search_form .wrapper .results_container .results .media_result .thumbnail{width:40px}
.page.home .search_form .wrapper .results_container .results .media_result .options{left:40px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{margin-right:5px;width:320px} .page.home .search_form .wrapper .results_container .results .media_result .options .title{margin-right:5px;width:320px}
} }
@media (min-width:480px) and (max-width:480px){.page.home .search_form .wrapper .results_container .results .media_result{height:44px} @media (min-width:480px) and (max-width:480px){.page.home .search_form .wrapper .results_container .results .media_result{height:44px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{width:140px;margin-right:2px} .page.home .search_form .wrapper .results_container .results .media_result .options .title{width:140px;margin-right:2px}
} }
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results .media_result .data{left:40px}
}
@media (max-width:480px){.page.home .search_form .wrapper .results_container .results .media_result{height:44px} @media (max-width:480px){.page.home .search_form .wrapper .results_container .results .media_result{height:44px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{width:140px;margin-right:2px} .page.home .search_form .wrapper .results_container .results .media_result .options .title{width:140px;margin-right:2px}
} }
@ -811,7 +809,7 @@ input[type=text],textarea{-webkit-appearance:none}
.mask .message,.mask .spinner{position:absolute;top:50%;left:50%} .mask .message,.mask .spinner{position:absolute;top:50%;left:50%}
.mask .message{color:#FFF;text-align:center;width:320px;margin:-49px 0 0 -160px;font-size:16px} .mask .message{color:#FFF;text-align:center;width:320px;margin:-49px 0 0 -160px;font-size:16px}
.mask .message h1{font-size:1.5em} .mask .message h1{font-size:1.5em}
.mask .spinner{width:22px;height:22px;display:block;background:#fff;margin-top:-11px;margin-left:-11px;outline:transparent solid 1px;-webkit-animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;-webkit-transform:scale(0);transform:scale(0)} .mask .spinner{width:22px;height:22px;display:block;background:#FFF;margin-top:-11px;margin-left:-11px;outline:transparent solid 1px;-webkit-animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;-webkit-transform:scale(0);transform:scale(0)}
.mask.with_message .spinner{margin-top:-88px} .mask.with_message .spinner{margin-top:-88px}
.mask.show{pointer-events:auto;opacity:1} .mask.show{pointer-events:auto;opacity:1}
.mask.show .spinner{-webkit-transform:scale(1);transform:scale(1)} .mask.show .spinner{-webkit-transform:scale(1);transform:scale(1)}
@ -935,7 +933,7 @@ input[type=text],textarea{-webkit-appearance:none}
.page.settings .option_list h2 .hint{font-weight:300} .page.settings .option_list h2 .hint{font-weight:300}
.page.settings .combined_table{margin-top:20px} .page.settings .combined_table{margin-top:20px}
.page.settings .combined_table .head{margin:0 10px 0 46px;font-size:.8em} .page.settings .combined_table .head{margin:0 10px 0 46px;font-size:.8em}
.page.settings .combined_table .head abbr{display:inline-block;font-weight:700;border-bottom:1px dotted #fff;line-height:140%;cursor:help;margin-right:10px;text-align:center} .page.settings .combined_table .head abbr{display:inline-block;font-weight:700;border-bottom:1px dotted #FFF;line-height:140%;cursor:help;margin-right:10px;text-align:center}
.page.settings .combined_table .head abbr:first-child{display:none} .page.settings .combined_table .head abbr:first-child{display:none}
.page.settings .combined_table input{min-width:0!important;display:inline-block;margin-right:10px} .page.settings .combined_table input{min-width:0!important;display:inline-block;margin-right:10px}
.page.settings .combined_table .automation_ids,.page.settings .combined_table .automation_urls,.page.settings .combined_table .host{width:200px} .page.settings .combined_table .automation_ids,.page.settings .combined_table .automation_urls,.page.settings .combined_table .host{width:200px}

5
py-dev-requirements.txt

@ -0,0 +1,5 @@
nose
rednose
nose-htmloutput
coverage
coveralls
Loading…
Cancel
Save