1

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.

4
  • 3
    Servers that bind to 127.0.0.1 or localhost are unreachable from outside their containers. You must set ADDRESS = '0.0.0.0'. Commented Apr 15, 2019 at 23:34
  • D'oh! Thanks, mate, you saved me. Make your comment an answer so I can Check it. Commented Apr 15, 2019 at 23:54
  • Do not add answer to your question. If you got solution, and its different from answers given by others then post regular answer and mark it accepted to close this question Commented Apr 16, 2019 at 0:07
  • Thank you, Marcin. I should've read the rules ;) Commented Apr 16, 2019 at 0:15

2 Answers 2

2

Problem solved by using ADDRESS = '0.0.0.0'. Kudos to David Maze.

Sign up to request clarification or add additional context in comments.

Comments

0

If Windows is the host, Docker is using a Linux virtual machine to host its environments, so "localhost" from Windows or vice versa won't do, they are different hosts.

You can get information about the VM docker is using with docker-machine.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.