I wrote a script for uploading/downloading files. It works on the system (Win10 here), but not from the docker container I put it in.
I used (a cut version of) Dockerfile and docker-compose.yml from another Python project, which is Django REST Framework one, there they work just fine. Python version 3.7.0. Only the standard library modules used. The Docker image is the official python-3.7-alpine.
The Python script (imports skipped):
ADDRESS, PORT = 'localhost', 5050
DATABASE = 'db.sqlite'
FILEDIR = 'Uploads'
class HttpHandler(BaseHTTPRequestHandler):
'''A tiny request handler for uploading and downloading files.'''
def __init__(self: '__main__.HttpHandler', *args, **kwargs) -> None:
'''
The handler class constructor. Before initialization checks if
the DATABASE file and the FILEDIR directory/folder both exist,
otherwise creates them.
'''
makedirs(FILEDIR, exist_ok=True)
if not path.isfile(DATABASE):
conn = sqlite3.connect(DATABASE)
with conn:
conn.execute('''CREATE TABLE filepaths (
uuid CHARACTER(36) PRIMARY KEY,
filepath TEXT NOT NULL,
filename TEXT NOT NULL,
extension TEXT,
upload_date TEXT
);''')
conn.close()
print(f'Database {DATABASE} created')
super().__init__(*args, **kwargs)
def read_from_db(self: '__main__.HttpHandler',
file_id: str) -> Union[tuple, None]:
'''Fetch the file record from the database.'''
try:
conn = sqlite3.connect(DATABASE)
with closing(conn):
cursor = conn.cursor()
query = f'''SELECT filepath, filename, extension, upload_date
FROM filepaths
WHERE uuid=:id;
'''
cursor.execute(query, {'id': file_id})
return cursor.fetchone()
except sqlite3.DatabaseError as error:
self.send_response(code=500, message='Database error')
self.end_headers()
print('Database error :', error)
def send_file(self: '__main__.HttpHandler',
file_id: str,
filepath: str,
filename: str,
extension: str) -> None:
'''Send the requested file to user.'''
try:
with open(filepath, 'rb') as file:
self.send_response(code=200)
self.send_header(
'Content-Disposition',
f'attachment; filename="{filename}.{extension}"'
)
self.end_headers()
data = file.read()
self.wfile.write(data)
except FileNotFoundError:
self.send_response(
code=410,
message=f'File with id {file_id} was deleted.'
)
self.end_headers()
def do_GET(self: '__main__.HttpHandler') -> None: # pylint: disable=C0103
'''
Check if a record for the given id exists in the DATABASE and
send the respective response to user; if 'download' parameter
provided, download the existing file to user from FILEPATH.
Usage is as follows:
CHECK
http://<ADDRESS>:<PORT>/?id=<file_id>
DOWNLOAD
http://<ADDRESS>:<PORT>/?id=<file_id>&download=1
'''
get_query = urlsplit(self.path).query
params = dict(parse_qsl(get_query))
if 'id' not in params:
self.send_response_only(code=200)
self.end_headers()
return
file_id = params['id']
db_response = self.read_from_db(file_id)
if not db_response:
self.send_response(code=204,
message=f'No files found with id {file_id}')
self.end_headers()
return
filepath, filename, extension, upload_date = db_response
if 'download' not in params:
self.send_response(
code=200,
message=f'{filename}.{extension} was uploaded at {upload_date}'
)
self.end_headers()
else:
self.send_file(file_id, filepath, filename, extension)
def do_POST(self: '__main__.HttpHandler') -> None: # pylint: disable=C0103
'''
Upload a file to FILEPATH and create the record for that
in the DATABASE, then send it's id in the response message.
Usage is as follows:
UPLOAD
POST request containing the file body to http://<ADDRESS>:<PORT>/
Content-Length must be provided in the headers;
If Content-Disposition is absent, the file will be saved as
"filename.not_provided"
'''
content_length = int(self.headers.get('Content-Length', 0))
if content_length == 0:
self.send_response(code=411, message='Length required')
self.end_headers()
return
content_disposition = self.headers.get('Content-Disposition',
'name="filename.not_provided"')
filename, extension = re.findall(r'name="(.+)\.(\S+)"',
content_disposition)[0]
file_content = self.rfile.read(content_length)
uuid = uuid4()
filepath = path.join(getcwd(), FILEDIR, f'{uuid}.{extension}')
with open(filepath, 'wb') as file:
file.write(file_content)
try:
with sqlite3.connect(DATABASE) as conn:
query = '''INSERT INTO filepaths VALUES (
:uuid,
:filepath,
:filename,
:extension,
:upload_date
);'''
conn.execute(query, {'uuid': str(uuid),
'filepath': filepath,
'filename': filename,
'extension': extension,
'upload_date': datetime.now()})
conn.close()
self.send_response(code=201, message=uuid)
self.end_headers()
except sqlite3.DatabaseError as error:
self.send_response(code=500, message='Database error')
self.end_headers()
print('Database error :', error)
if __name__ == "__main__":
with ThreadingTCPServer((ADDRESS, PORT), HttpHandler) as httpd:
print('Serving on port', PORT)
SERVER_THREAD = Thread(httpd.serve_forever(), daemon=True)
SERVER_THREAD.start()
Dockerfile:
FROM python:3.7-alpine
ENV PYTHONUNBUFFERED 1
RUN mkdir /server
WORKDIR /server
COPY . /server
RUN adduser -D user
RUN chown -R user:user /server
RUN chmod -R 755 /server
USER user
docker-compose.yml:
version: "3"
services:
server:
build:
context: .
ports:
- "5050:5050"
volumes:
- .:/server
command: >
sh -c "python server_threaded.py"
I used the requests library to make... ehm... requests, the code is simplistic:
import requests
print(requests.get('http://localhost:5050/'))
The output from the server side does not change:
$ docker-compose up
Recreating servers_server_1 ... done
Attaching to servers_server_1
server_1 | Serving on port 5050
Basically it doesn't respond anyhow. The client side error message:
requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
The client message if I run the script on the system:
<Response [200]>
I tried fiddling with ports, changing them here and there, used Postman and telnet, removed 'ENV PYTHONUNBUFFERED 1' from Dockerfile. Nothing works. I'm not a Docker captain obviously but the config looks to me as very basic. What am I doing wrong? Thanks.
localhostare unreachable from outside their containers. You must setADDRESS = '0.0.0.0'.