Switch to unified view

a/src/mediaserver/cdplugins/uprcl/uprclhttp.py b/src/mediaserver/cdplugins/uprcl/uprclhttp.py
1
#!/usr/bin/env python
1
# Copyright (C) 2017 J.F.Dockes
2
#   This program is free software; you can redistribute it and/or modify
3
#   it under the terms of the GNU General Public License as published by
4
#   the Free Software Foundation; either version 2 of the License, or
5
#   (at your option) any later version.
2
#
6
#
3
# Copyright (C) 2017 J.F.Dockes
7
#   This program is distributed in the hope that it will be useful,
8
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
9
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
#   GNU General Public License for more details.
4
#
11
#
5
# HTTP Range code:
12
#   You should have received a copy of the GNU General Public License
6
#  Portions Copyright (C) 2009,2010  Xyne
13
#   along with this program; if not, write to the
7
#  Portions Copyright (C) 2011 Sean Goller
14
#   Free Software Foundation, Inc.,
8
#  https://github.com/smgoller/rangehttpserver/blob/master/RangeHTTPServer.py
15
#   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
9
#
16
#
10
# This program is free software: you can redistribute it and/or modify
17
11
# it under the terms of the GNU General Public License as published by
12
# the Free Software Foundation, either version 3 of the License, or
13
# (at your option) any later version.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# GNU General Public License for more details.
19
#
20
# You should have received a copy of the GNU General Public License
21
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
from __future__ import print_function
18
from __future__ import print_function
23
19
24
import SocketServer
25
import BaseHTTPServer
26
import os
20
import os
27
import posixpath
28
import urllib
29
import urlparse
30
import shutil
21
import time
31
import mimetypes
22
import bottle
32
import sys
33
23
34
import mutagen
24
from uprclutils import uplog, embedded_open
25
import uprclinit
35
26
36
try:
27
@bottle.route('/')
37
    from cStringIO import StringIO
28
@bottle.post('/')
38
except ImportError:
29
@bottle.view('main')
39
    from StringIO import StringIO
30
def main():
31
    sub =  bottle.request.forms.get('sub')
32
    #uplog("Main: sub value is %s" % sub)
33
    if uprclinit.updaterunning():
34
        status = 'Updating'
35
    else:
36
        status = 'Ready'
40
37
41
from uprclutils import uplog,printable,embedded_open
38
    if sub == 'Update Index':
39
        uprclinit.start_update()
40
41
    if sub:
42
        headers = dict()
43
        headers["Location"] = '/'
44
        return bottle.HTTPResponse(status=302, **headers)
45
    else:
46
        return {'title':status, 'status':status,
47
                'friendlyname':uprclinit.g_friendlyname}
48
49
@bottle.route('/static/<filepath:path>')
50
def static(filepath):
51
    #uplog("control: static: filepath %s datadir %s" % (filepath, datadir))
52
    return bottle.static_file(filepath, root=os.path.join(datadir, 'static'))
42
53
43
54
44
__version__ = "0.1"
55
# Object for streaming data from a given subtree (topdirs entry more
56
# or less). This is needed just because as far as I can see, a
57
# callback can't know the route it was called for, so we record it
58
# when creating the object.
59
class Streamer(object):
60
    def __init__(self, root):
61
        self.root = root
45
62
46
class RangeHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
63
    def __call__(self, filepath):
64
        embedded = True if 'embed' in bottle.request.query else False
65
        if embedded:
66
            # Embedded image urls have had a .jpg or .png
67
            # appended. Remove it to restore the track path name.
68
            i = filepath.rfind('.')
69
            filepath = filepath[:i]
70
            apath = os.path.join(self.root,filepath)
71
            ctype, size, f = embedded_open(apath)
72
            fs = os.stat(apath)
73
            lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(fs.st_mtime))
74
            bottle.response.set_header("Last-Modified", lm)
75
            bottle.response.set_header("Content-type", ctype)
76
            bottle.response.set_header("Content-Length", size)
77
            return f
78
        uplog("Uprcl: streaming: %s" % os.path.join(self.root,filepath))
79
        return bottle.static_file(filepath, root=self.root)
47
80
48
    """Simple HTTP request handler with GET and HEAD commands.
81
    
49
82
50
    The MIME type for files is determined by calling the .guess_type() method.
83
# Bottle handle both the streaming and control requests.
84
def runbottle(host='0.0.0.0', port=9278, pthstr='', pathprefix=''):
85
    global datadir
86
    uplog("runbottle: host %s port %d pthstr %s pathprefix %s" %
87
          (host, port, pthstr, pathprefix))
88
    datadir = os.path.dirname(__file__)
89
    datadir = os.path.join(datadir, 'bottle')
90
    bottle.TEMPLATE_PATH = (os.path.join(datadir, 'views'),)
51
91
52
    The GET and HEAD requests are identical except that the HEAD
92
    # All the file urls must be like /some/prefix/path where
53
    request omits the actual contents of the file.
93
    # /some/prefix must be in the path translation map (which I'm not
54
94
    # sure what the use of is). By default the map is an identical
55
    """
95
    # translation of all topdirs entries. We create one route for each
56
96
    # prefix. As I don't know how a bottle method can retrieve the
57
    server_version = "RangeHTTP/" + __version__
97
    # route it was called from, we create a callable for each prefix.
58
98
    # Each route is built on the translation input, and the processor
59
    def do_GET(self):
99
    # uses the translated path as root
60
        """Serve a GET request."""
61
        f, start_range, end_range = self.send_head()
62
        if f:
63
            #uplog("do_GET: Got (%d,%d)" % (start_range,end_range))
64
            f.seek(start_range, 0)
65
            chunk = 0x1000
66
            total = 0
67
            while chunk > 0:
68
                if start_range + chunk > end_range:
69
                    chunk = end_range - start_range
70
                try:
71
                    self.wfile.write(f.read(chunk))
72
                except:
73
                    break
74
                total += chunk
75
                start_range += chunk
76
            f.close()
77
78
    def do_HEAD(self):
79
        """Serve a HEAD request."""
80
        f, start_range, end_range = self.send_head()
81
        if f:
82
            f.close()
83
84
    def send_head(self):
85
        """Common code for GET and HEAD commands.
86
87
        This sends the response code and MIME headers.
88
89
        Return value is either a file object (which has to be copied
90
        to the outputfile by the caller unless the command was HEAD,
91
        and must be closed by the caller under all circumstances), or
92
        None, in which case the caller has nothing further to do.
93
94
        """
95
96
        path,embedded = self.translate_path(self.path)
97
        #uplog("HTTP: translated: embedded %s path: %s" %
98
        #      (embedded, printable(path)))
99
100
        if not path or not os.path.exists(path):
101
            self.send_error(404)
102
            return (None, 0, 0)
103
104
        if not os.path.isfile(path):
105
            self.send_error(405)
106
            return (None, 0, 0)
107
108
        f = None
109
        try:
110
            if embedded:
111
                ctype, size, f = embedded_open(path)
112
                fs = os.stat(path)
113
                #uplog("embedded, got ctype %s size %s" %(ctype, size))
114
            else:
115
                ctype = self.guess_type(path)
116
                f = open(path, 'rb')
117
                fs = os.fstat(f.fileno())
118
                size = int(fs[6])
119
        except Exception as err:
120
            self.send_error(404, "File not found")
121
            return (None, 0, 0)
122
123
        if "Range" in self.headers:
124
            self.send_response(206)
125
        else:
126
            self.send_response(200)
127
128
        self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
129
        self.send_header("Content-type", ctype)
130
        start_range = 0
131
        end_range = size
132
        self.send_header("Accept-Ranges", "bytes")
133
        if "Range" in self.headers:
134
            s, e = self.headers['range'][6:].split('-', 1)
135
            sl = len(s)
136
            el = len(e)
137
            if sl > 0:
138
                start_range = int(s)
139
                if el > 0:
140
                    end_range = int(e) + 1
141
            elif el > 0:
142
                ei = int(e)
143
                if ei < size:
144
                    start_range = size - ei
145
        self.send_header("Content-Range",
146
                         'bytes ' + str(start_range) + '-' +
147
                         str(end_range - 1) + '/' + str(size))
148
        self.send_header("Content-Length", end_range - start_range)
149
        self.end_headers()
150
        #uplog("Sending Bytes %d to %d" % (start_range, end_range))
151
        return (f, start_range, end_range)
152
153
154
    def translate_path(self, opath):
155
        path = urllib.unquote(opath)
156
        # pathprefix is something like /uprcl
157
        path = path.replace(self.uprclpathprefix, '', 1)
158
159
        q = urlparse.urlparse(path)
160
        path = q.path
161
        embedded = False
162
        pq = urlparse.parse_qs(q.query)
163
        if 'embed' in pq:
164
            embedded = True
165
166
        for fsp,htp in self.uprclpathmap.iteritems():
167
            if path.startswith(fsp):
168
                path = path.replace(fsp, htp, 1)
169
                if embedded:
170
                    # Embedded image urls have had a .jpg or .png
171
                    # appended. Remove it to restore the track path
172
                    # name.
173
                    i = path.rfind('.')
174
                    path = path[:i]
175
                return path, embedded
176
177
        # Security feature here: never allow access to anything not in
178
        # the path map
179
        uplog("HTTP: translate_path: %s not found in path map" % opath)
180
        return None, None
181
182
183
    def guess_type(self, path):
184
        """Guess the type of a file.
185
186
        Argument is a PATH (a filename).
187
188
        Return value is a string of the form type/subtype,
189
        usable for a MIME Content-type header.
190
191
        The default implementation looks the file's extension
192
        up in the table self.extensions_map, using application/octet-stream
193
        as a default; however it would be permissible (if
194
        slow) to look inside the data to make a better guess.
195
196
        """
197
198
        base, ext = posixpath.splitext(path)
199
        if ext in self.extensions_map:
200
            return self.extensions_map[ext]
201
        ext = ext.lower()
202
        if ext in self.extensions_map:
203
            return self.extensions_map[ext]
204
        else:
205
            return self.extensions_map['']
206
207
    if not mimetypes.inited:
208
        mimetypes.init() # try to read system mime.types
209
    extensions_map = mimetypes.types_map.copy()
210
    extensions_map.update({
211
        '': 'application/octet-stream', # Default
212
        '.mp4': 'video/mp4',
213
        '.ogg': 'video/ogg',
214
        })
215
216
217
class ThreadingSimpleServer(SocketServer.ThreadingMixIn,
218
                            BaseHTTPServer.HTTPServer):
219
    # Override handle_error as the default version writes to stdout !
220
    def handle_error(self, request, client_address):
221
        # Actually, we generally don't care about errors...
222
        return
223
    
224
        uplog('-'*40)
225
        uplog('Exception happened during processing of request from %s' %
226
              str(client_address))
227
        import traceback
228
        traceback.print_exc() # XXX But this goes to stderr! (jf: yep :)
229
        uplog('-'*40)
230
231
232
def runHttp(host='', port=8080, pthstr='', pathprefix=''):
233
234
    lpth = pthstr.split(',')
100
    lpth = pthstr.split(',')
235
    pathmap = {}
236
    for ptt in lpth:
101
    for ptt in lpth:
237
        l = ptt.split(':')
102
        l = ptt.split(':')
238
        pathmap[l[0]] = l[1]
103
        rt = l[0]
239
    # Set pathmap as request handler class variable
104
        if rt[-1] != '/':
240
    RangeHTTPRequestHandler.uprclpathmap = pathmap
105
            rt += '/'
241
    RangeHTTPRequestHandler.uprclpathprefix = pathprefix
106
        rt += '<filepath:path>'
242
    
107
        uplog("runbottle: adding route for: %s"%rt)
243
    server = ThreadingSimpleServer((host, port), RangeHTTPRequestHandler)
108
        # We build the streamer with the translated 
244
    while 1:
109
        streamer = Streamer(l[1])
245
        server.handle_request()
110
        bottle.route(rt, 'GET', streamer)
246
111
247
if __name__ == '__main__':
112
    bottle.run(server='paste', host=host, port=port)
248
    if len(sys.argv) != 5:
249
        print("Usage: uprclhttp.py <host> <port> <pthmap> <pthprefix>",
250
              file=sys.stderr)
251
        sys.exit(1)
252
    runHttp(host=sys.argv[1], port = int(sys.argv[2]), pthstr=sys.argv[3],
253
            pathprefix=sys.argv[4])