1
1
import dataclasses as dc
2
- from email . message import Message
2
+ import io
3
3
from pathlib import Path
4
- from email .parser import BytesParser
5
4
import subprocess
6
5
import tempfile
7
- from typing import IO
8
6
import magic
7
+ import os
8
+ import logging
9
9
10
+ _logger = logging .getLogger (__name__ )
10
11
11
12
class Loader :
12
- def handleRequest (
13
- self , url : str , headers : dict [str , str ]
14
- ) -> tuple [int , dict [str , str ], bytes ]:
15
- return (
16
- 404 ,
17
- {
18
- "mime" : "text/html" ,
19
- },
20
- b"<html><body>404 Not Found</body></html>" ,
21
- )
22
-
13
+ def handleRequest (self , url : str ) -> tuple [dict [str , str ], bytes ]:
14
+ raise NotImplementedError ()
23
15
24
16
@dc .dataclass
25
17
class StaticDir (Loader ):
@@ -28,43 +20,141 @@ class StaticDir(Loader):
28
20
def __init__ (self , path : Path ):
29
21
self ._path = path
30
22
31
- def handleRequest (
32
- self , url : str , headers : dict [ str , str ]
33
- ) -> tuple [ int , dict [ str , str ], bytes ]:
34
- path = self . _path / url
23
+ def handleRequest (self , url : str ) -> tuple [ dict [ str , str ], bytes ]:
24
+ # Http path starts with '/' what ends up being interpreted as the root of the FS when appending, so we rmv it
25
+ path = self . _path / url [ 1 :]
26
+
35
27
if not path .exists ():
36
- return (
37
- 404 ,
38
- {
39
- "mime" : "text/html" ,
40
- },
41
- b"<html><body>404 Not Found</body></html>" ,
42
- )
28
+ raise FileNotFoundError ()
43
29
with open (path , "rb" ) as f :
44
30
return (
45
- 200 ,
46
31
{
47
32
"mime" : magic .Magic (mime = True ).from_file (path ),
48
33
},
49
34
f .read (),
50
35
)
51
36
52
37
38
+ MAX_BUFFER_SIZE = 1024
39
+ class HttpMessage ():
40
+
41
+ def __init__ (self ):
42
+ self .headers = {}
43
+
44
+ def _readHeaderLines (self , reader : io .TextIOWrapper ) -> list [str ]:
45
+ lines = []
46
+
47
+ while True :
48
+ request_line = reader .readline ().decode ('utf-8' )
49
+
50
+ if len (request_line ) == 0 :
51
+ raise EOFError ("Input stream has ended" )
52
+
53
+ if request_line == "\r \n " :
54
+ break
55
+
56
+ lines .append (request_line )
57
+
58
+ return lines
59
+
60
+ def _addToHeader (self , header_line : str ) -> None :
61
+ key , value = header_line .split (':' )
62
+ self .headers [key .strip ()] = value .strip ()
63
+
64
+ def readHeader (self , reader : io .TextIOWrapper ) -> None :
65
+ raise NotImplementedError ()
66
+
67
+ def _readSingleChunk (self , reader : io .TextIOWrapper ) -> bytes :
68
+ def read_chunk_content (rem_size ):
69
+ chunk = b""
70
+
71
+ while rem_size > 0 :
72
+ bs = min (MAX_BUFFER_SIZE , rem_size )
73
+ byte = reader .read (bs )
74
+ chunk += byte
75
+
76
+ rem_size -= bs
77
+ return chunk
78
+
79
+ size = int (reader .readline ()[:- 2 ])
80
+ chunk = read_chunk_content (size )
81
+
82
+ reader .read (2 )
83
+
84
+ return chunk
85
+
86
+ def readChunkedBody (self , reader : io .TextIOWrapper ) -> bytes :
87
+ encoded_body = b""
88
+ while True :
89
+ chunk = self ._readSingleChunk (reader )
90
+
91
+ if chunk is None :
92
+ return None
93
+
94
+ if len (chunk ) == 0 :
95
+ break
96
+
97
+ encoded_body += chunk
98
+
99
+ return encoded_body
100
+
101
+ class HttpRequest (HttpMessage ):
102
+
103
+ def __init__ (self , method = None , path = None , version = None ):
104
+ super ().__init__ ()
105
+ self .method = method
106
+ self .path = path
107
+ self .version = version
108
+
109
+ def readHeader (self , reader : io .TextIOWrapper ) -> None :
110
+ header_lines = self ._readHeaderLines (reader )
111
+ self .method , self .path , self .version = header_lines [0 ].split (' ' )
112
+
113
+ for line in header_lines [1 :]:
114
+ self ._addToHeader (line )
115
+
116
+
117
+ RESPONSE_MESSAGES = {
118
+ 200 : 'OK' ,
119
+ 404 : 'Not Found'
120
+ }
121
+
122
+ class HttpResponse (HttpMessage ):
123
+
124
+ def __init__ (self , code : int , headers : dict [str , str ] = {}, version = "1.1" ):
125
+ super ().__init__ ()
126
+ self .headers |= headers
127
+ self .version = version
128
+ self .code = code
129
+ self .body = None
130
+
131
+ def addHeader (self , key : str , value : str ) -> None :
132
+ self .headers [key ] = value
133
+
134
+ def addBody (self , body : bytes ) -> None :
135
+ if not isinstance (body , bytes ):
136
+ raise ValueError ("Body must be in bytes" )
137
+ self .body = body
138
+ self .addHeader ("Content-Length" , len (body ))
139
+
140
+ def __bytes__ (self ) -> bytes :
141
+ def firstLine ():
142
+ return f"HTTP/{ self .version } { self .code } { RESPONSE_MESSAGES .get (self .code , 'No Message' )} " .encode ()
143
+
144
+ def headers ():
145
+ return (f"{ key } : { value } " .encode () for key , value in self .headers .items ())
146
+
147
+ return b"\r \n " .join ([firstLine (), * headers (), b"" , self .body or b"" ])
148
+
149
+
53
150
def _run (
54
151
args : list [str ],
55
152
loader = Loader (),
56
153
) -> bytes :
57
- def _readRequest (fd : IO ) -> Message [str , str ] | None :
58
- # Read the request header from the file descriptor
59
- parser = BytesParser ()
60
- return parser .parse (fd )
61
-
62
- def _sendResponse (fd : IO , status : int , headers : dict [str , str ], body : bytes ):
63
- fd .write (f"HTTP/2 { status } \r \n " .encode ())
64
- for key , value in headers .items ():
65
- fd .write (f"{ key } : { value } \r \n " .encode ())
66
- fd .write (b"\r \n " )
67
- fd .write (body )
154
+
155
+ def sendResponse (stdin : io .TextIOWrapper , response : HttpResponse ):
156
+ stdin .write (bytes (response ))
157
+ stdin .flush ()
68
158
69
159
with subprocess .Popen (
70
160
args ,
@@ -84,58 +174,63 @@ def _sendResponse(fd: IO, status: int, headers: dict[str, str], body: bytes):
84
174
if stdin is None :
85
175
raise ValueError ("stdin is None" )
86
176
87
- while True :
88
- request = _readRequest (stdout )
89
- if request is None :
90
- raise ValueError ("request is None" )
91
-
92
- if request .preamble is None :
93
- raise ValueError ("request.preamble is None" )
94
-
95
- preamble = request .preamble .split (" " )
96
- if preamble [0 ] == b"GET" :
97
- _sendResponse (stdin , * loader .handleRequest (preamble [1 ], dict (request )))
98
- elif preamble [0 ] == b"POST" :
99
- payload = request .get_payload ()
100
- if not isinstance (payload , bytes ):
101
- raise ValueError ("payload is not bytes" )
102
- proc .terminate ()
103
- return payload
104
- else :
105
- raise ValueError ("Invalid request" )
106
-
107
-
108
- def find () -> Path :
109
- return Path (__file__ ).parent / "bin"
110
-
111
-
112
- def print (
177
+ # The only exception we are recovering from for now is FileNotFound, which is implemented in PM's HttPipe flow
178
+ try :
179
+ while True :
180
+ request = HttpRequest ()
181
+ request .readHeader (stdout )
182
+
183
+ if request .method == "GET" :
184
+ try :
185
+ headers , asset = loader .handleRequest (request .path )
186
+ except FileNotFoundError :
187
+ response = HttpResponse (404 )
188
+ else :
189
+ response = HttpResponse (200 , headers )
190
+ response .addBody (asset )
191
+
192
+ sendResponse (stdin , response )
193
+ elif request .method == "POST" :
194
+ payload = request .readChunkedBody (stdout )
195
+ proc .terminate ()
196
+ return payload
197
+ else :
198
+ raise ValueError ("Invalid request" )
199
+ except Exception as e :
200
+ proc .terminate ()
201
+ _logger .debug (stderr .read ().decode ('utf-8' ))
202
+ raise e
203
+
204
+
205
+ def printPM (
113
206
document : bytes | str | Path ,
114
- mime : str = "text/html" ,
207
+ bin : str ,
115
208
loader : Loader = StaticDir (Path .cwd ()),
116
- bin : Path = find () ,
209
+ * args : str ,
117
210
** kwargs : str ,
118
211
) -> bytes :
119
- extraArgs = []
212
+
213
+ extraArgs = list (args )
120
214
for key , value in kwargs .items ():
121
215
extraArgs .append (f"--{ key } " )
122
216
extraArgs .append (str (value ))
123
217
124
218
if isinstance (document , Path ):
125
219
return _run (
126
- [str ( bin ) , "print" , "-i" , str (document ), "-o" , "out.pdf" ] + extraArgs ,
220
+ [bin , "print" , str (document )] + extraArgs ,
127
221
loader ,
128
222
)
129
223
else :
130
- with tempfile .NamedTemporaryFile (delete = False ) as f :
224
+ with tempfile .NamedTemporaryFile (dir = loader . _path ) as f :
131
225
if isinstance (document , str ):
132
226
document = document .encode ()
133
227
f .write (document )
228
+ f .flush ()
229
+
134
230
return _run (
135
- [str (bin ), "print" , "-i" , f .name , "-o" , "out.pdf" ] + extraArgs ,
231
+ [str (bin ), "print" , os . path . basename ( f .name ) ] + extraArgs ,
136
232
loader ,
137
233
)
138
- return b""
139
234
140
235
141
- __all__ = ["Loader" , "StaticDir" , "print " ]
236
+ __all__ = ["Loader" , "StaticDir" , "printPM " ]
0 commit comments