Module lakota.server

The server sub-module implement a flask application that expose The main methods of POD. It can be launched from the cli like this:

$ lakota serve "local-repo file:///.lakota"
 * Serving Flask app "Lakota Repository" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it
   in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
INFO:2021-01-22 16:04:08:  * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)

local-repo is the route at which the API will exposed and file:///.lakota the source repo.

Once the server is started you can query it, for example you can do (while the server is running in another shell):

$ lakota -r http://localhost:8080/local-repo ls
...

It can also be used as a local proxy to speed-up access to remote locations:

$ lakota serve "faster-bucket memory:// s3:///bucket_name"

You can also use file:////tmp/local-cache instead of memory:// to provide persistant caching.

You can expose several repo under different prefixes:

$ lakota serve "local-repo file:///.lakota" "faster-bucket memory:// s3:///bucket_name"

Beware: no authentication nor encryption is provided, and the server expose full read and write access to the underlying repository.

Expand source code
"""
The server sub-module implement a flask application that expose
The main methods of `lakota.pod.POD`.  It can be launched from the cli
like this:

``` shell
$ lakota serve "local-repo file:///.lakota"
 * Serving Flask app "Lakota Repository" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it
   in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
INFO:2021-01-22 16:04:08:  * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
```

`local-repo` is the route at which the API will exposed and
`file:///.lakota` the source repo.

Once the server is started you can query it, for example you can do
(while the server is running in another shell):

``` shell
$ lakota -r http://localhost:8080/local-repo ls
...
```

It can also be used as a local proxy to speed-up access to remote locations:

``` shell
$ lakota serve "faster-bucket memory:// s3:///bucket_name"
```

You can also use `file:////tmp/local-cache` instead of `memory://` to
provide persistant caching.

You can expose several repo under different prefixes:

``` shell
$ lakota serve "local-repo file:///.lakota" "faster-bucket memory:// s3:///bucket_name"
```

**Beware**: no authentication nor encryption is provided, and the server
expose full read and write access to the underlying repository.
"""

import base64
from urllib.parse import urlsplit

from flask import Blueprint, Flask, request

from lakota import Repo

pod_bp = Blueprint(f"Lakota POD", __name__)


@pod_bp.route("/<action>", methods=["GET", "POST"])
def pod(repo, action, relpath=None):
    """
    summary: Interact with low-level POD object (GET)
    ---
    parameters:
      - in: action
        name: action
        schema:
          type: string
        required: true
        description: Action to perform (ls, read, rm or walk)
      - in: path
        name: relpath
        schema:
          type: string
        required: false
        description: Relative path
    """
    relpath = request.args.get("path")
    if action == "ls":
        try:
            relpath = "." if relpath is None else relpath
            names = repo.pod.ls(relpath)
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"body": names}

    elif action == "read":
        try:
            payload = repo.pod.read(relpath)
            payload = base64.b64encode(payload).decode("ascii")
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"body": payload, "b64encoded": True}

    elif action == "rm":
        recursive = request.args.get("recursive", "").lower() == "true"
        missing_ok = request.args.get("missing_ok", "").lower() == "true"
        try:
            repo.pod.rm(relpath, recursive=recursive, missing_ok=missing_ok)
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"status": "ok"}

    elif action == "mv":
        from_path = request.args["from_path"]
        to_path = request.args["to_path"]
        missing_ok = request.args.get("missing_ok", "").lower() == "true"

        try:
            repo.pod.mv(from_path, to_path, missing_ok=missing_ok)
        except FileNotFoundError:
            return 'Path "{from_path}" not found', 404
        return {"status": "ok"}

    elif action == "write":
        force = request.args.get("force", "").lower() == "true"
        try:
            info = repo.pod.write(relpath, request.data, force=force)
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"body": info}

    elif action == "walk":
        pod = repo.pod
        try:
            if relpath:
                pod = pod.cd(relpath)
            max_depth = request.args.get("max_depth")
            if max_depth is not None:
                max_depth = int(max_depth)
            names = list(pod.walk(max_depth=max_depth))
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"body": names}

    else:
        return 'Action "{action}" not supported', 404


def index(prefixes):
    items = "\n".join(f"<li><a href={p}>{p}</a></li>" for p in prefixes)
    return f"<h4>Available endpoints:</h4><ul>{items}</ul>"


def run(repo_map, web_uri=None, debug=False):
    parts = urlsplit(web_uri)
    if not parts.scheme == "http":
        # if no scheme is given, hostname and port are not interpreted correctly
        msg = "Incorrect web uri, it should start with 'http://'"
        raise ValueError(msg)

    # Instanciate app and blueprint. Run app
    app = Flask("Lakota Repository")
    prefixes = []
    for name, uri in repo_map.items():
        prefix = parts.path + "/" + name.strip("/")
        repo = Repo(uri)
        app.register_blueprint(pod_bp, url_prefix=prefix, url_defaults={"repo": repo})
        prefixes.append(prefix)

    # Add index page
    app.route("/")(lambda: index(prefixes))
    app.run(parts.hostname, debug=debug, port=parts.port)

Functions

def index(prefixes)
Expand source code
def index(prefixes):
    items = "\n".join(f"<li><a href={p}>{p}</a></li>" for p in prefixes)
    return f"<h4>Available endpoints:</h4><ul>{items}</ul>"
def pod(repo, action, relpath=None)

summary: Interact with low-level POD object (GET)

parameters: - in: action name: action schema: type: string required: true description: Action to perform (ls, read, rm or walk) - in: path name: relpath schema: type: string required: false description: Relative path

Expand source code
@pod_bp.route("/<action>", methods=["GET", "POST"])
def pod(repo, action, relpath=None):
    """
    summary: Interact with low-level POD object (GET)
    ---
    parameters:
      - in: action
        name: action
        schema:
          type: string
        required: true
        description: Action to perform (ls, read, rm or walk)
      - in: path
        name: relpath
        schema:
          type: string
        required: false
        description: Relative path
    """
    relpath = request.args.get("path")
    if action == "ls":
        try:
            relpath = "." if relpath is None else relpath
            names = repo.pod.ls(relpath)
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"body": names}

    elif action == "read":
        try:
            payload = repo.pod.read(relpath)
            payload = base64.b64encode(payload).decode("ascii")
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"body": payload, "b64encoded": True}

    elif action == "rm":
        recursive = request.args.get("recursive", "").lower() == "true"
        missing_ok = request.args.get("missing_ok", "").lower() == "true"
        try:
            repo.pod.rm(relpath, recursive=recursive, missing_ok=missing_ok)
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"status": "ok"}

    elif action == "mv":
        from_path = request.args["from_path"]
        to_path = request.args["to_path"]
        missing_ok = request.args.get("missing_ok", "").lower() == "true"

        try:
            repo.pod.mv(from_path, to_path, missing_ok=missing_ok)
        except FileNotFoundError:
            return 'Path "{from_path}" not found', 404
        return {"status": "ok"}

    elif action == "write":
        force = request.args.get("force", "").lower() == "true"
        try:
            info = repo.pod.write(relpath, request.data, force=force)
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"body": info}

    elif action == "walk":
        pod = repo.pod
        try:
            if relpath:
                pod = pod.cd(relpath)
            max_depth = request.args.get("max_depth")
            if max_depth is not None:
                max_depth = int(max_depth)
            names = list(pod.walk(max_depth=max_depth))
        except FileNotFoundError:
            return 'Path "{relpath}" not found', 404
        return {"body": names}

    else:
        return 'Action "{action}" not supported', 404
def run(repo_map, web_uri=None, debug=False)
Expand source code
def run(repo_map, web_uri=None, debug=False):
    parts = urlsplit(web_uri)
    if not parts.scheme == "http":
        # if no scheme is given, hostname and port are not interpreted correctly
        msg = "Incorrect web uri, it should start with 'http://'"
        raise ValueError(msg)

    # Instanciate app and blueprint. Run app
    app = Flask("Lakota Repository")
    prefixes = []
    for name, uri in repo_map.items():
        prefix = parts.path + "/" + name.strip("/")
        repo = Repo(uri)
        app.register_blueprint(pod_bp, url_prefix=prefix, url_defaults={"repo": repo})
        prefixes.append(prefix)

    # Add index page
    app.route("/")(lambda: index(prefixes))
    app.run(parts.hostname, debug=debug, port=parts.port)