Refactor Static files to make them cleaner and faster
This commit is contained in:
parent
4012a6d350
commit
f9576f4dc0
212
static_file.py
212
static_file.py
|
@ -10,7 +10,6 @@
|
|||
'''
|
||||
import os
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
|
||||
from nereid.helpers import slugify, send_file
|
||||
from werkzeug import abort
|
||||
|
@ -20,50 +19,20 @@ from trytond.config import CONFIG
|
|||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
def get_nereid_path():
|
||||
"Returns base path for nereid"
|
||||
cursor = Transaction().cursor
|
||||
return os.path.join(CONFIG['data_path'], cursor.database_name)
|
||||
|
||||
|
||||
def make_folder_path(folder_name):
|
||||
"Returns the folder path for given folder"
|
||||
return os.path.join(get_nereid_path(), folder_name)
|
||||
|
||||
|
||||
def make_file_path(folder_name, file_name):
|
||||
"Returns the file path for the given folder, file"
|
||||
return os.path.join(make_folder_path(folder_name), file_name)
|
||||
|
||||
|
||||
def make_file(file_name, file_binary, folder):
|
||||
"""
|
||||
Writes file to the FS
|
||||
|
||||
:param file_name: Name of the file
|
||||
:param file_binary: Binary content to save (Base64 encoded)
|
||||
:param folder: folder name
|
||||
"""
|
||||
file_binary = base64.decodestring(file_binary)
|
||||
file_path = make_file_path(folder, file_name)
|
||||
with open(file_path, 'wb') as file_writer:
|
||||
file_writer.write(file_binary)
|
||||
return file_path
|
||||
|
||||
|
||||
# pylint: disable-msg=E1101
|
||||
|
||||
class NereidStaticFolder(ModelSQL, ModelView):
|
||||
"Static folder for Nereid"
|
||||
_name = "nereid.static.folder"
|
||||
_description = __doc__
|
||||
_rec_name = 'folder_name'
|
||||
|
||||
name = fields.Char('Description', required=True, select=1)
|
||||
folder_name = fields.Char('Folder Name', required=True, select=1,
|
||||
on_change_with=['name', 'folder_name'])
|
||||
comments = fields.Text('Comments')
|
||||
folder_name = fields.Char(
|
||||
'Folder Name', required=True, select=1,
|
||||
on_change_with=['name', 'folder_name']
|
||||
)
|
||||
description = fields.Char('Description', select=1)
|
||||
files = fields.One2Many('nereid.static.file', 'folder', 'Files')
|
||||
folder_path = fields.Function(fields.Char('Folder Path'), 'get_path')
|
||||
|
||||
def __init__(self):
|
||||
super(NereidStaticFolder, self).__init__()
|
||||
|
@ -81,14 +50,6 @@ class NereidStaticFolder(ModelSQL, ModelView):
|
|||
'folder_cannot_change': "Folder name cannot be changed"
|
||||
})
|
||||
|
||||
def get_path(self, ids, name):
|
||||
"""Return the path of the folder
|
||||
"""
|
||||
result = { }
|
||||
for folder in self.browse(ids):
|
||||
result[folder.id] = make_folder_path(folder.folder_name)
|
||||
return result
|
||||
|
||||
def on_change_with_folder_name(self, vals):
|
||||
"""
|
||||
Fills the name field with a slugified name
|
||||
|
@ -101,7 +62,7 @@ class NereidStaticFolder(ModelSQL, ModelView):
|
|||
def check_folder_name(self, ids):
|
||||
'''
|
||||
Check the validity of folder name
|
||||
Allowing the use of / or . will be risky as that could
|
||||
Allowing the use of / or . will be risky as that could
|
||||
eventually lead to previlege escalation
|
||||
|
||||
:param ids: ID of current record.
|
||||
|
@ -111,55 +72,18 @@ class NereidStaticFolder(ModelSQL, ModelView):
|
|||
return False
|
||||
return True
|
||||
|
||||
def create(self, vals):
|
||||
"""
|
||||
Check if the folder exists.
|
||||
If not, create a new one in data path of tryton.
|
||||
|
||||
:param vals: values of the current record
|
||||
"""
|
||||
folder_path = make_folder_path(vals.get('folder_name'))
|
||||
|
||||
# Create the nereid folder if it doesnt exist
|
||||
if not os.path.isdir(get_nereid_path()):
|
||||
os.mkdir(get_nereid_path())
|
||||
|
||||
# Create the folder if it doesnt exist
|
||||
if not os.path.isdir(folder_path):
|
||||
os.mkdir(folder_path)
|
||||
|
||||
return super(NereidStaticFolder, self).create(vals)
|
||||
|
||||
def write(self, ids, vals):
|
||||
"""
|
||||
Check if the folder_name has been modified.
|
||||
Check if the folder_name has been modified.
|
||||
If yes, raise an error.
|
||||
|
||||
:param vals: values of the current record
|
||||
"""
|
||||
if vals.get('folder_name'):
|
||||
# TODO: Support this feature in future versions
|
||||
self.raise_user_error('folder_cannot_change')
|
||||
return super(NereidStaticFolder, self).write(ids, vals)
|
||||
|
||||
def scan_files_from_fs(self, folder_ids):
|
||||
"""
|
||||
Scans File system for images and syncs them
|
||||
|
||||
:param folder_ids: ID of the System Folder
|
||||
"""
|
||||
file_object = self.pool.get('nereid.static.file')
|
||||
|
||||
for folder in self.browse(folder_ids):
|
||||
existing_filenames = [f.name for f in folder.files]
|
||||
|
||||
folder_path = make_folder_path(folder.folder_name)
|
||||
for content in os.listdir(folder_path):
|
||||
full_path = os.path.join(folder_path, content)
|
||||
|
||||
if (os.path.isfile(full_path)) and \
|
||||
(content not in existing_filenames):
|
||||
file_object.create({'name': content, 'folder': folder.id})
|
||||
return True
|
||||
|
||||
NereidStaticFolder()
|
||||
|
||||
|
@ -170,11 +94,18 @@ class NereidStaticFile(ModelSQL, ModelView):
|
|||
_description = __doc__
|
||||
|
||||
name = fields.Char('File Name', select=True, required=True)
|
||||
file_ = fields.Function(fields.Binary('File'),
|
||||
'get_field_binary', 'set_content')
|
||||
folder = fields.Many2One('nereid.static.folder', 'Folder', required=True)
|
||||
file_path = fields.Function(fields.Char('File Path'), 'get_fields',)
|
||||
relative_path = fields.Function(fields.Char('Relative Path'), 'get_fields')
|
||||
folder = fields.Many2One(
|
||||
'nereid.static.folder', 'Folder', select=True, required=True
|
||||
)
|
||||
|
||||
# This function field returns the field contents. This is useful if the
|
||||
# field is going to be displayed on the clients.
|
||||
file_binary = fields.Function(
|
||||
fields.Binary('File'), 'get_file_binary', 'set_file_binary'
|
||||
)
|
||||
|
||||
# Full path to the file in the filesystem
|
||||
file_path = fields.Function(fields.Char('File Path'), 'get_file_path',)
|
||||
|
||||
def __init__(self):
|
||||
super(NereidStaticFile, self).__init__()
|
||||
|
@ -191,60 +122,69 @@ class NereidStaticFile(ModelSQL, ModelView):
|
|||
(2) file name contains '/'""",
|
||||
})
|
||||
|
||||
def set_content(self, ids, name, value):
|
||||
def get_nereid_base_path(self):
|
||||
"""
|
||||
Creates the file for the function field
|
||||
"""
|
||||
for file_ in self.browse(ids):
|
||||
make_file(file_.name, value, file_.folder.folder_name)
|
||||
Returns base path for nereid, where all the static files would be
|
||||
stored.
|
||||
|
||||
def get_fields(self, ids, names):
|
||||
By Default it is:
|
||||
|
||||
<Tryton Data Path>/<Database Name>/nereid
|
||||
"""
|
||||
cursor = Transaction().cursor
|
||||
return os.path.join(
|
||||
CONFIG['data_path'], cursor.database_name, "nereid"
|
||||
)
|
||||
|
||||
def set_file_binary(self, ids, name, value):
|
||||
"""
|
||||
Setter for the functional binary field.
|
||||
|
||||
:param ids: List of ids. But usually has just one
|
||||
:param name: Ignored
|
||||
:param value: The base64 encoded value
|
||||
"""
|
||||
for f in self.browse(ids):
|
||||
file_binary = base64.decodestring(value)
|
||||
# If the folder does not exist, create it recursively
|
||||
directory = os.path.dirname(f.file_path)
|
||||
if not os.path.isdir(directory):
|
||||
os.makedirs(directory)
|
||||
with open(f.file_path, 'wb') as file_writer:
|
||||
file_writer.write(file_binary)
|
||||
|
||||
def get_file_binary(self, ids, name):
|
||||
'''
|
||||
Function to compute function fields for sale ids
|
||||
Getter for the binary_file field. This fetches the file from the
|
||||
file system, encodes it in base64 and returns it.
|
||||
|
||||
:param ids: the ids of the sales
|
||||
:param names: the list of field name to compute
|
||||
:return: a dictionary with all field names as key and
|
||||
a dictionary as value with id as key
|
||||
:return: Dictionary with ID as key and base64 encoded data
|
||||
'''
|
||||
res = defaultdict(dict)
|
||||
for name in names:
|
||||
res[name] = { }
|
||||
|
||||
for file_ in self.browse(ids):
|
||||
file_path = os.path.join(
|
||||
make_file_path(file_.folder.folder_name, file_.name))
|
||||
|
||||
if 'file_path' in names:
|
||||
res['file_path'][file_.id] = file_path
|
||||
|
||||
|
||||
if 'relative_path' in names:
|
||||
res['relative_path'][file_.id] = '/'.join([
|
||||
file_.folder.folder_name,
|
||||
file_.name])
|
||||
|
||||
res = {}
|
||||
for f in self.browse(ids):
|
||||
with open(f.file_path, 'rb') as file_reader:
|
||||
res[f.id] = base64.encodestring(file_reader.read())
|
||||
return res
|
||||
|
||||
def get_field_binary(self, ids, name):
|
||||
def get_file_path(self, ids, name):
|
||||
"""
|
||||
This could be part of the above function, but this is an
|
||||
expensive process which must not affect the rest of the processes
|
||||
Returns the full path to the file in the file system
|
||||
|
||||
:param ids: the ids of the sales
|
||||
:return: Dictionary with ID as key and binary
|
||||
"""
|
||||
result = {}
|
||||
for file_ in self.browse(ids):
|
||||
file_path = os.path.join(
|
||||
make_file_path(file_.folder.folder_name, file_.name))
|
||||
with open(file_path, 'rb') as file_handler:
|
||||
result[file_.id] = base64.encodestring(
|
||||
file_handler.read()
|
||||
)
|
||||
return result
|
||||
res = {}
|
||||
for f in self.browse(ids):
|
||||
res[f.id] = os.path.join(
|
||||
self.get_nereid_base_path(), f.folder.folder_name, f.name
|
||||
)
|
||||
return res
|
||||
|
||||
def check_file_name(self, ids):
|
||||
'''
|
||||
Check the validity of folder name
|
||||
Allowing the use of / or . will be risky as that could
|
||||
Allowing the use of / or . will be risky as that could
|
||||
eventually lead to previlege escalation
|
||||
|
||||
:param ids: ID of current record.
|
||||
|
@ -255,9 +195,15 @@ class NereidStaticFile(ModelSQL, ModelView):
|
|||
return True
|
||||
|
||||
def send_static_file(self, folder, name):
|
||||
'''
|
||||
Send the static file
|
||||
'''
|
||||
"""
|
||||
Invokes the send_file method in nereid.helpers to send a file as the
|
||||
response to the reuqest. The file is sent in a way which is as
|
||||
efficient as possible. For example nereid will use the X-Send_file
|
||||
header to make nginx send the file if possible.
|
||||
|
||||
:param folder: folder_name of the folder
|
||||
:param name: name of the file
|
||||
"""
|
||||
#TODO: Separate this search and find into separate cached method
|
||||
|
||||
ids = self.search([
|
||||
|
|
|
@ -13,6 +13,8 @@ from test_auth import TestAuth
|
|||
from test_address import TestAddress
|
||||
from test_currency import TestCurrency
|
||||
from test_i18n import TestI18N
|
||||
from test_static_file import TestStaticFile
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
|
@ -28,6 +30,9 @@ def suite():
|
|||
suite.addTests(
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestI18N)
|
||||
)
|
||||
suite.addTests(
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestStaticFile)
|
||||
)
|
||||
return suite
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_static_file
|
||||
|
||||
Test the static file feature of nereid
|
||||
|
||||
:copyright: (c) 2012 by Openlabs Technologies & Consulting (P) LTD
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import base64
|
||||
import unittest2 as unittest
|
||||
|
||||
from trytond.config import CONFIG
|
||||
CONFIG.options['db_type'] = 'sqlite'
|
||||
CONFIG.options['data_path'] = '/tmp/temp_tryton_data/'
|
||||
|
||||
from trytond.modules import register_classes
|
||||
register_classes()
|
||||
from nereid.testing import testing_proxy, TestCase
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class TestStaticFile(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestStaticFile, cls).setUpClass()
|
||||
|
||||
testing_proxy.install_module('nereid') # Install module
|
||||
|
||||
with Transaction().start(testing_proxy.db_name, 1, None) as txn:
|
||||
company = testing_proxy.create_company('Test Company')
|
||||
testing_proxy.set_company_for_user(1, company)
|
||||
|
||||
cls.guest_user = testing_proxy.create_guest_user(company=company)
|
||||
|
||||
cls.site = testing_proxy.create_site(
|
||||
'localhost',
|
||||
application_user = 1, guest_user = cls.guest_user
|
||||
)
|
||||
|
||||
txn.cursor.commit()
|
||||
|
||||
def get_app(self, **options):
|
||||
options.update({
|
||||
'SITE': 'testsite.com',
|
||||
'GUEST_USER': self.guest_user,
|
||||
})
|
||||
return testing_proxy.make_app(**options)
|
||||
|
||||
def setUp(self):
|
||||
self.static_folder_obj = testing_proxy.pool.get('nereid.static.folder')
|
||||
self.static_file_obj = testing_proxy.pool.get('nereid.static.file')
|
||||
|
||||
def test_0010_static_file(self):
|
||||
"""
|
||||
Create a static folder, and a static file
|
||||
And check if it can be fetched
|
||||
"""
|
||||
with Transaction().start(testing_proxy.db_name, 1, None) as txn:
|
||||
folder_id = self.static_folder_obj.create({
|
||||
'folder_name': 'test',
|
||||
'description': 'Test Folder'
|
||||
})
|
||||
encoded_data = base64.encodestring('test-content')
|
||||
file_id = self.static_file_obj.create({
|
||||
'name': 'test.png',
|
||||
'folder': folder_id,
|
||||
'file_binary': encoded_data
|
||||
})
|
||||
static_file = self.static_file_obj.browse(file_id)
|
||||
self.assertEqual(static_file.file_binary, encoded_data)
|
||||
|
||||
txn.cursor.commit()
|
||||
|
||||
app = self.get_app()
|
||||
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/en_US/static-file/test/test.png')
|
||||
self.assertEqual(rv.data, 'test-content')
|
||||
self.assertEqual(rv.headers['Content-Type'], 'image/png')
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
|
||||
|
||||
def suite():
|
||||
"Nereid test suite"
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTests(
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestStaticFile)
|
||||
)
|
||||
return suite
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.TextTestRunner(verbosity=2).run(suite())
|
7
urls.xml
7
urls.xml
|
@ -125,13 +125,6 @@
|
|||
<field name="url_map" ref="default_url_map" />
|
||||
</record>
|
||||
|
||||
<record id="url_rule_static_file" model="nereid.url_rule">
|
||||
<field name="rule">/<language>/static-files/<folder>/<name></field>
|
||||
<field name="endpoint">nereid.static.file.send_static_file</field>
|
||||
<field name="url_map" ref="default_url_map"/>
|
||||
<field name="sequence" eval="10"/>
|
||||
</record>
|
||||
|
||||
<record id="static_file_url" model="nereid.url_rule">
|
||||
<field name="rule">/<language>/static-file/<folder>/<name></field>
|
||||
<field name="endpoint">nereid.static.file.send_static_file</field>
|
||||
|
|
Loading…
Reference in New Issue