def test_etag(): rv = send_file(txt_path, environ) rv.close() assert rv.headers["ETag"].count("-") == 2 rv = send_file(txt_path, environ, etag=False) rv.close() assert "ETag" not in rv.headers rv = send_file(txt_path, environ, etag="unique") rv.close() assert rv.headers["ETag"] == '"unique"'
def test_object_attachment_requires_name(): with pytest.raises(TypeError, match="attachment"): send_file( io.BytesIO(b"test"), environ, mimetype="text/plain", as_attachment=True, ) rv = send_file( io.BytesIO(b"test"), environ, as_attachment=True, download_name="test.txt", ) assert rv.headers["Content-Disposition"] == f"attachment; filename=test.txt" rv.close()
def test_object(file_factory): rv = send_file(file_factory(), environ, mimetype="text/plain", use_x_sendfile=True) rv.direct_passthrough = False assert rv.data assert rv.mimetype == "text/plain" assert "x-sendfile" not in rv.headers rv.close()
def test_content_encoding(as_attachment): rv = send_file( txt_path, environ, download_name="logo.svgz", as_attachment=as_attachment ) rv.close() assert rv.mimetype == "image/svg+xml" assert rv.content_encoding == ("gzip" if not as_attachment else None)
def test_path(path): rv = send_file(path, environ) assert rv.mimetype == "text/html" assert rv.direct_passthrough rv.direct_passthrough = False assert rv.data == html_path.read_bytes() rv.close()
def test_max_age(value, public): rv = send_file(txt_path, environ, max_age=value) rv.close() assert ("no-cache" in rv.headers["Cache-Control"]) != public assert rv.cache_control.public == public assert rv.cache_control.max_age == value assert rv.expires assert rv.status_code == 200
def test_non_ascii_name(name, ascii, utf8): rv = send_file(html_path, environ, as_attachment=True, download_name=name) rv.close() content_disposition = rv.headers["Content-Disposition"] assert f"filename={ascii}" in content_disposition if utf8: assert f"filename*=UTF-8''{utf8}" in content_disposition else: assert "filename*=UTF-8''" not in content_disposition
def company_logo(self, dbname=None, **kw): imgname = 'logo' imgext = '.png' placeholder = functools.partial(get_resource_path, 'web', 'static', 'img') dbname = request.db uid = (request.session.uid if dbname else None) or odoo.SUPERUSER_ID if not dbname: response = http.Stream.from_path( placeholder(imgname + imgext)).get_response() else: try: # create an empty registry registry = odoo.modules.registry.Registry(dbname) with registry.cursor() as cr: company = int( kw['company']) if kw and kw.get('company') else False if company: cr.execute( """SELECT logo_web, write_date FROM res_company WHERE id = %s """, (company, )) else: cr.execute( """SELECT c.logo_web, c.write_date FROM res_users u LEFT JOIN res_company c ON c.id = u.company_id WHERE u.id = %s """, (uid, )) row = cr.fetchone() if row and row[0]: image_base64 = base64.b64decode(row[0]) image_data = io.BytesIO(image_base64) mimetype = guess_mimetype(image_base64, default='image/png') imgext = '.' + mimetype.split('/')[1] if imgext == '.svg+xml': imgext = '.svg' response = send_file(image_data, filename=imgname + imgext, mimetype=mimetype, mtime=row[1]) else: response = http.Stream.from_path( placeholder('nologo.png')).get_response() except Exception: response = http.Stream.from_path( placeholder(imgname + imgext)).get_response() return response
def test_root_path(tmp_path): # This is a private API, it should only be used by Flask. d = tmp_path / "d" d.mkdir() (d / "test.txt").write_bytes(b"test") rv = send_file("d/test.txt", environ, _root_path=tmp_path) rv.direct_passthrough = False assert rv.data == b"test" rv.close() rv = send_from_directory("d", "test.txt", environ, _root_path=tmp_path) rv.direct_passthrough = False assert rv.data == b"test" rv.close()
def test_no_cache_conditional_default(): rv = send_file( txt_path, EnvironBuilder( headers={"If-Modified-Since": http_date(datetime.datetime(2020, 7, 12))} ).get_environ(), last_modified=datetime.datetime(2020, 7, 11), ) rv.close() assert "no-cache" in rv.headers["Cache-Control"] assert not rv.cache_control.public assert not rv.cache_control.max_age assert not rv.expires assert rv.status_code == 304
def wsgi(self, environ, start_response): request_initial = Request(environ) request_initial.path = request_initial.path.replace('/..', '') response = Response('bad request', 400) page = Page('', 400) path = request_initial.path resource = join_path(Config.base_path, path) request = PHFSRequest(environ, path, resource) self.request = request if os.path.isdir(resource): # If there's a index.html inside folder, show that for i in self.indexes: if os.path.isfile(join_path(resource, i)): path = join_path(path, i) resource = join_path(resource, i) uni_param = UniParam([], interpreter=self.interpreter, request=request, filelist=FileList([]), statistics=self.statistics) levels_virtual = path.split('/') levels_real = resource.split('/') if resource[-1:] != '/' and os.path.isdir(resource): response = Response('', 302, {'Location': path + '/'}) return self.return_response(request, response, environ, start_response) if not Account.can_access( self.get_current_account(request)[0], resource, True): response = self.unauth_response(request) return self.return_response(request, response, environ, start_response) if request.args.get('tpl', '') == 'list': uni_param.interpreter = self.itp_filelist if request.method == 'POST': if len(request.files) > 0: # File upload if not if_upload_allowed_in(request.path_real, Config): response = Response('forbidden', 403) else: upload_result = {} for i in request.files: single_file = request.files[i] filename = purify_filename(single_file.filename) if filename == '': continue elif filename in self.indexes: upload_result[filename] = ( False, I18n.get_string( 'file_name_or_extension_forbidden')) continue try: single_file.save(join_path(resource, filename)) upload_result[filename] = (True, '') except Exception as e: upload_result[filename] = (False, str(e)) page = self.interpreter.get_page( 'upload-results', UniParam([upload_result], interpreter=self.interpreter, request=request, statistics=self.statistics)) response = Response(page.content, page.status, page.headers, mimetype=mimeLib.getmime('*.html')) return self.return_response(request, response, environ, start_response) if request.form.get('action', '') == 'delete': if not Account.can_access( self.get_current_account(request)[0], resource, False): response = Response('forbidden', 403) else: filelist = request.form.getlist('selection') try: for i in filelist: smartremove(Config.base_path + i) response = Response('ok', 200) except Exception as err: response = Response(str(err), 500) return self.return_response(request, response, environ, start_response) if 'mode' in request.args: # urlvar mode mode = request.args['mode'] if mode == 'section': section_name = request.args.get('id', '') page = uni_param.interpreter.section_to_page( section_name, uni_param) response = Response(page.content, page.status, page.headers, mimetype=mimeLib.getmime(section_name)) elif mode == 'login': account_name = request.form.get('user') token_hash = request.form.get('passwordSHA256') expected_hash = hashLib.BaseHashToTokenHash( Account.get_account_detail(account_name)[0], request.cookies.get('HFS_SID_', '')).get() if account_name not in Account.accounts: response = Response('username not found', 200) elif token_hash != expected_hash: response = Response('bad password', 200) else: sid = hashlib.sha256( bytes([random.randint(0, 255) for _ in range(32)])).hexdigest() self.statistics.accounts[sid] = (account_name, request.host) response = Response('ok', 200) response.headers.add_header( 'Set-Cookie', 'HFS_SID_=%s; HttpOnly; Max-Age=0' % request.cookies.get('HFS_SID_', '')) response.headers.add_header('Set-Cookie', 'HFS_SID_=%s; HttpOnly' % sid) elif mode == 'logout': sid = request.cookies.get('HFS_SID_', '') if sid in self.statistics.accounts: del self.statistics.accounts[sid] response = Response( 'ok', 200, {'Set-Cookie': 'HFS_SID_=%s; HttpOnly; Max-Age=0' % sid}) elif mode == 'archive': tmp = tempfile.TemporaryFile(mode='w+b') tar = tarfile.open(mode='w', fileobj=tmp) path_real = request.path_real_dir + '/' filelist = request.form.getlist('selection') final_list_without_dots = [ ((Config.base_path if x[0:1] == '/' else path_real) + x) for x in filelist if x[0:1] != '.' ] final_list_with_dots = [ ((Config.base_path if x[0:1] == '/' else path_real) + x) for x in filelist ] shown_files = final_list_without_dots if Config.hide_dots else final_list_with_dots for i in shown_files: is_recursive = 'recursive' in request.args or bool( Config.recur_archive) tar.add(i, i, recursive=is_recursive) tar.close() # Pointer is at the end of file tmp.seek(0) # Read at start response = send_file( tmp, environ, mimetype=mimeLib.getmime('*.tar'), as_attachment=True, download_name=os.path.basename(request.path_virtual_dir) + '.selection.tar') return self.return_response(request, response, environ, start_response) elif 'search' in request.args: # Search, with re.findall directory = request.path_real_dir if not os.path.isdir(directory): response = self.not_found_response(request) return self.return_response(request, response, environ, start_response) pattern = re.compile(wildcard2re(request.args['search']), re.I) recursive = 'recursive' in request.args or bool( Config.recur_search) items_folder = [] items_file = [] if recursive: for dirpath, dirnames, filenames in os.walk(directory): for i in dirnames: if re.findall(pattern, i): items_folder.append(os.path.join(dirpath, i)) for i in filenames: if re.findall(pattern, i): items_file.append(os.path.join(dirpath, i)) else: for i in os.scandir(directory): if re.findall(pattern, i.name): if i.is_dir: items_folder.append(i.path) else: items_file.append(i.path) path_real_dir = request.path_real_dir + '/' shown_files = [ x for x in (items_folder + items_file) if x[0:1] != '.' ] if Config.hide_dots else (items_folder + items_file) paths = [x.replace('\\', '/') for x in shown_files] items = [ItemEntry(x, x, path_real_dir) for x in paths] filelist = FileList(items) uni_param.filelist = filelist page = uni_param.interpreter.get_page('', uni_param) response = Response(page.content, page.status, page.headers, mimetype=mimeLib.getmime('*.html')) return self.return_response(request, response, environ, start_response) elif 'filter' in request.args: # Filter, with re.fullmatch directory = request.path_real_dir if not os.path.isdir(directory): response = self.not_found_response(request) return self.return_response(request, response, environ, start_response) pattern = re.compile(wildcard2re(request.args['filter']), re.I) recursive = 'recursive' in request.args or bool( Config.recur_search) items_folder = [] items_file = [] if recursive: for dirpath, dirnames, filenames in os.walk(directory): for i in dirnames: if re.fullmatch(pattern, i): items_folder.append(os.path.join(dirpath, i)) for i in filenames: if re.fullmatch(pattern, i): items_file.append(os.path.join(dirpath, i)) else: for i in os.scandir(directory): if re.fullmatch(pattern, i.name): if i.is_dir: items_folder.append(i.path) else: items_file.append(i.path) path_real_dir = request.path_real_dir + '/' shown_files = [ x for x in (items_folder + items_file) if x[0:1] != '.' ] if Config.hide_dots else (items_folder + items_file) paths = [x.replace('\\', '/') for x in shown_files] items = [ItemEntry(x, x, path_real_dir) for x in paths] filelist = FileList(items) uni_param.filelist = filelist page = uni_param.interpreter.get_page('', uni_param) response = Response(page.content, page.status, page.headers, mimetype=mimeLib.getmime('*.html')) return self.return_response(request, response, environ, start_response) elif levels_virtual[-1][0:1] == '~': # Command command = levels_virtual[-1][1:] if len(levels_virtual) == 2: # Section call, only at root global builtin_sections section = uni_param.interpreter.get_section( command, uni_param, True, False) if section != None: page = Page(section.content, 200) response = Response(page.content, page.status, page.headers, mimetype=mimeLib.getmime(path)) elif command in builtin_sections: response = send_file(command, environ, mimeLib.getmime(command)) else: response = self.not_found_response(request) if command == 'upload' and if_upload_allowed_in( request.path_real, Config): page = uni_param.interpreter.get_page('upload', uni_param) response = Response(page.content, page.status, page.headers, mimetype=mimeLib.getmime('*.html')) elif command == 'folder.tar': tmp = tempfile.TemporaryFile(mode='w+b') tar = tarfile.open(mode='w', fileobj=tmp) path_real = request.path_real_dir + '/' shown_files = [ x for x in os.listdir(request.path_real_dir) if x[0:1] != '.' ] if Config.hide_dots else os.listdir(request.path_real_dir) for i in shown_files: is_recursive = 'recursive' in request.args or bool( Config.recur_archive) tar.add(path_real + i, i, recursive=is_recursive) tar.close() # Pointer is at the end of file tmp.seek(0) # Read at start response = send_file(tmp, environ, mimetype=mimeLib.getmime('*.tar')) elif command == 'files.lst': uni_param.interpreter = self.itp_filelist path_real_dir = request.path_real_dir + '/' shown_files = [ x for x in os.listdir(request.path_real_dir) if x[0:1] != '.' ] if Config.hide_dots else os.listdir(request.path_real_dir) paths = [join_path(path_real_dir, x) for x in shown_files] items = [ItemEntry(x, x, path_real_dir) for x in paths] filelist = FileList(items) uni_param.filelist = filelist page = uni_param.interpreter.get_page('', uni_param) response = Response(page.content, page.status, page.headers, mimetype=mimeLib.getmime('*.txt')) return self.return_response(request, response, environ, start_response) elif resource != None: # Filelist or send file or 404 if os.path.exists(resource): if os.path.isdir(resource): # List files if 'no list' not in uni_param.interpreter.sections[ ''].params: path_real_dir = request.path_real_dir + '/' shown_files = [ x for x in os.listdir(path_real_dir) if x[0:1] != '.' ] if Config.hide_dots else os.listdir(path_real_dir) paths = [ join_path(path_real_dir, x) for x in shown_files ] items = [ItemEntry(x, x, path_real_dir) for x in paths] filelist = FileList(items) uni_param.filelist = filelist page = uni_param.interpreter.get_page('', uni_param) response = Response(page.content, page.status, page.headers, mimetype=mimeLib.getmime( '*.txt' if uni_param.interpreter == self.itp_filelist else '*.html')) elif levels_real[-1].lower().endswith( '.zip') and Config.preview_zip: # Preview zip file if configured if resource not in self.cached_zip_files: try: zip_file = zipfile.ZipFile(resource, 'r') except zipfile.BadZipFile: response = Response( I18n.get_string('zip_file_is_broken'), 202) return self.return_response( request, response, environ, start_response) self.cached_zip_files[resource] = (zip_file, ) zip_file_data = self.cached_zip_files[resource] if 'getitem' in request.args: zip_file = zip_file_data[0] filename = request.args['getitem'] try: response = Response( zip_file.read(filename), 200, mimetype=mimeLib.getmime(filename)) except KeyError: response = self.not_found_response(request) else: items = [ ZipItemEntry(x, resource, path) for x in zip_file_data[0].filelist if x.filename[-1:] != '/' ] filelist = FileList(items) uni_param.filelist = filelist page = uni_param.interpreter.get_page('', uni_param) response = Response( page.content, page.status, page.headers, mimetype=mimeLib.getmime( '*.txt' if uni_param.interpreter == self.itp_filelist else '*.html')) else: # A file response = send_file(resource, environ) else: # 404 response = self.not_found_response(request) return self.return_response(request, response, environ, start_response) else: response = self.not_found_response(request) return self.return_response(request, response, environ, start_response)
def test_disposition_name(as_attachment, value): rv = send_file(txt_path, environ, as_attachment=as_attachment) assert rv.headers["Content-Disposition"] == f"{value}; filename=test.txt" rv.close()
def test_text_mode_fails(file_factory): with file_factory() as f, pytest.raises(ValueError, match="binary mode"): send_file(f, environ, mimetype="text/plain")
def test_object_mimetype_from_name(): rv = send_file(io.BytesIO(b"test"), environ, download_name="test.txt") assert rv.mimetype == "text/plain" rv.close()
def test_object_without_mimetype(): with pytest.raises(TypeError, match="detect the MIME type"): send_file(io.BytesIO(b"test"), environ)
def test_content_encoding(): rv = send_file(txt_path, environ, download_name="logo.svgz") rv.close() assert rv.mimetype == "image/svg+xml" assert rv.content_encoding == "gzip"
def test_last_modified(): last_modified = datetime.datetime(1999, 1, 1) rv = send_file(txt_path, environ, last_modified=last_modified) assert rv.last_modified == last_modified rv.close()
def test_x_sendfile(): rv = send_file(html_path, environ, use_x_sendfile=True) assert rv.headers["x-sendfile"] == str(html_path) assert rv.data == b"" rv.close()
def test_max_age_callable(): # This is a private API, it should only be used by Flask. rv = send_file(txt_path, environ, max_age=lambda p: 10) rv.close() assert rv.cache_control.max_age == 10