blob: dc740fb00249066515c6300c48198e9921a2b677 [file] [log] [blame]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
from xml.sax.saxutils import escape
import bottle
from twitter.common import log
from twitter.common.http import HttpServer
from .templating import HttpTemplate
MB = 1024 * 1024
DEFAULT_CHUNK_LENGTH = MB
MAX_CHUNK_LENGTH = 16 * MB
def _read_chunk(filename, offset=None, length=None):
offset = offset or -1
length = length or -1
try:
length = long(length)
offset = long(offset)
except ValueError:
return {}
if not os.path.isfile(filename):
return {}
try:
fstat = os.stat(filename)
except Exception as e:
log.error('Could not read from %s: %s', filename, e)
return {}
if offset == -1:
offset = fstat.st_size
if length == -1:
length = fstat.st_size - offset
with open(filename, "r") as fp:
fp.seek(offset)
try:
data = fp.read(length)
except IOError as e:
log.error('Failed to read %s: %s', filename, e, exc_info=True)
return {}
if data:
return dict(offset=offset, length=len(data), data=escape(data.decode('utf8', 'replace')))
return dict(offset=offset, length=0)
class TaskObserverFileBrowser(object):
"""
Mixin for Thermos observer File browser.
"""
@HttpServer.route("/logs/:task_id/:process/:run/:logtype")
@HttpServer.mako_view(HttpTemplate.load('logbrowse'))
def handle_logs(self, task_id, process, run, logtype):
types = self._observer.logs(task_id, process, int(run))
if logtype not in types:
bottle.abort(404, "No such log type: %s" % logtype)
base, path = types[logtype]
_, filename = self._observer.valid_path(task_id, os.path.join(base, path))
return {
'task_id': task_id,
'filename': filename,
'process': process,
'run': run,
'logtype': logtype
}
@HttpServer.route("/logdata/:task_id/:process/:run/:logtype")
def handle_logdata(self, task_id, process, run, logtype):
offset = self.Request.GET.get('offset', -1)
length = self.Request.GET.get('length', -1)
types = self._observer.logs(task_id, process, int(run))
if logtype not in types:
return {}
chroot, path = types[logtype]
return _read_chunk(os.path.join(chroot, path), offset, length)
@HttpServer.route("/file/:task_id/:path#.+#")
@HttpServer.mako_view(HttpTemplate.load('filebrowse'))
def handle_file(self, task_id, path):
if path is None:
bottle.abort(404, "No such file")
return {
'task_id': task_id,
'filename': path,
}
@HttpServer.route("/filedata/:task_id/:path#.+#")
def handle_filedata(self, task_id, path):
if path is None:
return {}
offset = self.Request.GET.get('offset', -1)
length = self.Request.GET.get('length', -1)
chroot, path = self._observer.valid_file(task_id, path)
if chroot is None or path is None:
return {}
return _read_chunk(os.path.join(chroot, path), offset, length)
@HttpServer.route("/browse/:task_id")
@HttpServer.route("/browse/:task_id/:path#.*#")
@HttpServer.mako_view(HttpTemplate.load('filelist'))
def handle_dir(self, task_id, path=None):
if path == "":
path = None
chroot, path = self._observer.valid_path(task_id, path)
if chroot is None or path is None:
bottle.abort(404, "Sandbox does not exist.")
return dict(task_id=task_id, chroot=chroot, path=path)
@HttpServer.route("/download/:task_id/:path#.+#")
def handle_download(self, task_id, path=None):
chroot, path = self._observer.valid_path(task_id, path)
if path is None:
bottle.abort(404, "No such file")
return bottle.static_file(path, root=chroot, download=True, mimetype='application/octet-stream')