[
  {
    "path": ".dockerignore",
    "content": "node_modules/\nDockerfile\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:3-alpine\n\nRUN apk add --no-cache nginx supervisor\n\nADD . /app\nWORKDIR /app\n\nRUN apk add --no-cache nodejs \\\n && npm install \\\n && npm run build \\\n && rm -rf node_modules \\\n && apk del nodejs\n\nRUN apk add --no-cache --virtual build-dep gcc linux-headers libc-dev \\\n && apk add --no-cache jpeg-dev zlib-dev \\\n && pip install -r /app/requirements.txt \\\n && apk del build-dep\n\nEXPOSE 80\n\nCMD /usr/bin/supervisord -c /app/docker/supervisor.conf\n"
  },
  {
    "path": "LICENSE.md",
    "content": "\"THE BEER-WARE LICENSE\" (Revision 42):\nkaiyou wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think\nthis stuff is worth it, you can buy me a beer in return.\n"
  },
  {
    "path": "README.md",
    "content": "Image upload with Flask\n=======================\n\nNo database, no additional features. Plain and simple image upload.\n\n![Screenshot](screenshot.png)\n\nUse the Docker image\n--------------------\n\nSimply allocate a data directory and create the thumbnails\nsub-directory:\n\n    mkdir -p /path/to/data/thumbnails\n\nThen run the image server:\n\n    docker run --name=tedimg -d -v /path/to/data:/data kaiyou/tedimg\n\nBuild from source\n-----------------\n\nNodeJS and NPM are required to build from source :\n\n    git clone git@github.com:kaiyou/tedimg.git\n    cd tedimg\n    npm install\n    gulp\n    docker build\n"
  },
  {
    "path": "docker/nginx.conf",
    "content": "user nginx;\nworker_processes 4;\npid /run/nginx.pid;\ndaemon off;\n\nevents {\n\tworker_connections 768;\n}\n\nhttp {\n\tsendfile on;\n\ttcp_nopush on;\n\ttcp_nodelay on;\n\tkeepalive_timeout 65;\n\ttypes_hash_max_size 2048;\n\tserver_tokens off;\n\tinclude /etc/nginx/mime.types;\n\tdefault_type application/octet-stream;\n\n\taccess_log /dev/stdout;\n\terror_log /dev/stderr;\n\n\tgzip on;\n\tgzip_disable \"msie6\";\n\n\tmap $http_x_forwarded_proto $proxy_x_forwarded_proto {\n\t\tdefault $http_x_forwarded_proto;\n\t\t''      $scheme;\n\t}\n\n  server {\n    listen 80;\n\n\t\tclient_max_body_size 20M;\n\n\t\tadd_header X-Frame-Options 'SAMEORIGIN';\n\t\tadd_header X-Content-Type-Options 'nosniff';\n\t\tadd_header X-Permitted-Cross-Domain-Policies 'none';\n\t\tadd_header X-XSS-Protection '1; mode=block';\n\t\tadd_header Referrer-Policy 'same-origin';\n\n    location /data {\n      root /;\n    }\n\n    location /static {\n      root /app/tedimg;\n    }\n\n    location / {\n      proxy_pass         http://127.0.0.1:8000/;\n\n\t\t\tproxy_set_header Host $http_host;\n\t\t\tproxy_set_header X-Real-IP $remote_addr;\n\t\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\t\t\tproxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;\n    }\n  }\n}\n"
  },
  {
    "path": "docker/supervisor.conf",
    "content": "[supervisord]\npidfile=/var/run/supervisord.pid\nnodaemon=true\nloglevel=DEBUG\n\n[program:nginx]\ncommand=nginx -c /app/docker/nginx.conf\nredirect_stderr=true\n\n[program:flask]\ndirectory=/app\ncommand = gunicorn -w 4 -b 127.0.0.1:8000 tedimg:app\nredirect_stderr=true\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"tedimg\",\n  \"scripts\": {\n    \"watch\": \"webpack -w\",\n    \"build\": \"webpack -p\"\n  },\n  \"dependencies\": {\n    \"css-loader\": \"^0.28.11\",\n    \"file-loader\": \"^1.1.11\",\n    \"jquery\": \"^3.3.1\",\n    \"js-loader\": \"^0.1.1\",\n    \"materialize-css\": \"^0.100.2\",\n    \"resolve-url-loader\": \"^2.3.0\",\n    \"style-loader\": \"^0.21.0\",\n    \"url-loader\": \"^1.0.1\",\n    \"webpack\": \"^4.9.1\",\n    \"webpack-cli\": \"^2.1.4\",\n    \"webpack-dev-server\": \"^3.1.4\"\n  }\n}\n"
  },
  {
    "path": "requirements.txt",
    "content": "Flask==1.0.2\nPillow==5.1.0\nrequests==2.18.4\ngunicorn==19.8.1\nWerkzeug==0.14.1\n"
  },
  {
    "path": "resources/main.css",
    "content": "/* Browser specific (not valid) styles to make preformatted text wrap */\n\npre {\n white-space: pre-wrap;       /* css-3 */\n white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */\n white-space: -pre-wrap;      /* Opera 4-6 */\n white-space: -o-pre-wrap;    /* Opera 7 */\n word-wrap: break-word;       /* Internet Explorer 5.5+ */\n}\n\ninput.snippet {\n  font-family: monospace;\n}\n\n::-webkit-input-placeholder {\n    color:    #444;\n}\n:-moz-placeholder {\n    color:    #444;\n}\n::-moz-placeholder {\n    color:    #444;\n}\n:-ms-input-placeholder {\n    color:    #444;\n}\n\n/* Materialize icons */\n\n/* fallback */\n@font-face {\n  font-family: 'Material Icons';\n  font-style: normal;\n  font-weight: 400;\n  src: url('https://fonts.gstatic.com/s/materialicons/v38/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2') format('woff2');\n}\n\n.material-icons {\n  font-family: 'Material Icons';\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;\n  line-height: 1;\n  letter-spacing: normal;\n  text-transform: none;\n  display: inline-block;\n  white-space: nowrap;\n  word-wrap: normal;\n  direction: ltr;\n  -moz-font-feature-settings: 'liga';\n  -moz-osx-font-smoothing: grayscale;\n}\n"
  },
  {
    "path": "resources/main.js",
    "content": "// Import general CSS\nimport 'materialize-css';\nimport 'materialize-css/dist/css/materialize.css';\n\n// Import specific CSS\nimport './main.css';\n\n// Javascript libs\nimport $ from 'jquery';\n\n$(document).ready(function() {\n\n  $(\"form#upload input\").change(function() {\n    $(\"form#upload\").submit();\n  });\n\n  $(\"form#upload input[type=text]\").on('paste', function() {\n    setTimeout(function () {\n        $(\"form#upload\").submit();\n    }, 100);\n  });\n\n  $(\"input.snippet\").click(function() {\n    this.select();\n  })\n\n});\n"
  },
  {
    "path": "run.py",
    "content": "from tedimg import app\nfrom flask import send_from_directory\n\nimport os\n\napp.config.update(\n    SITE_NAME=\"TeDomum Images\",\n    FULL_STORAGE=\"./tedimg/static/images\",\n    THUMB_STORAGE=\"./tedimg/static/images/thumb\",\n    FULL_WEB=\"static/images\",\n    THUMB_WEB=\"static/images/thumb\"\n)\n\napp.run(debug=True)\n"
  },
  {
    "path": "tedimg/__init__.py",
    "content": "from flask import Flask\n\nimport os\n\napp = Flask(__name__)\napp.debug = \"FLASK_DEBUG\" in os.environ\n\napp.config.update(\n    SITE_NAME=os.environ.get(\"SITE_NAME\", \"TedImg\"),\n    SOURCE_URL=\"https://git.tedomum.net/kaiyou/tedimg\",\n    HELP_URL=\"https://git.tedomum.net/kaiyou/tedimg\",\n    FULL_STORAGE=os.environ.get(\"FULL_STORAGE\", \"/data\"),\n    THUMB_STORAGE=os.environ.get(\"THUMB_STORAGE\", \"/data/thumb\"),\n    FULL_WEB=os.environ.get(\"FULL_WEB\", \"data\"),\n    THUMB_WEB=os.environ.get(\"THUMB_WEB\", \"data/thumb\"),\n    THUMB_SIZE=100\n)\n\nif (app.debug):\n    from werkzeug import debug\n    app.wsgi_app = debug.DebuggedApplication(app.wsgi_app, True)\n\nimport tedimg.views\n"
  },
  {
    "path": "tedimg/images.py",
    "content": "from tedimg import app\nfrom PIL import Image, ImageSequence\n\nimport os\nimport binascii\nimport requests\nimport io\nimport urllib\n\n\ndef get_image(root, name):\n    \"\"\" Try and get basic image attributes.\n    \"\"\"\n    filename = urllib.parse.quote(os.path.basename(name))\n    return (os.path.join(root, app.config[\"FULL_WEB\"], filename),\n            os.path.join(root, app.config[\"THUMB_WEB\"], filename))\n\n\ndef image_from_file(file_storage):\n    \"\"\" Try and read the uploaded file.\n    \"\"\"\n    image = Image.open(file_storage)\n    return image\n\n\ndef image_from_url(url):\n    \"\"\" Try and download an image from the given url.\n    \"\"\"\n    response = requests.get(url)\n    image = Image.open(io.BytesIO(response.content))\n    return image\n\n\ndef save_with_thumbnail(image, filename):\n    dest = \".\"\n    while os.path.exists(os.path.join(app.config[\"FULL_STORAGE\"], dest)):\n        filename, _ = os.path.splitext(filename)\n        ext = image.format.lower()\n        random = binascii.hexlify(os.urandom(3)).decode('utf8')\n        dest = \"%s-%s.%s\" % (filename, random, ext)\n    # Grab some configuration\n    full_file = os.path.join(app.config[\"FULL_STORAGE\"], dest)\n    thumb_file = os.path.join(app.config[\"THUMB_STORAGE\"], dest)\n    thumb_size = app.config[\"THUMB_SIZE\"]\n    # Save the image and thumbnail\n    if image.format == 'GIF':\n        image.save(full_file, format=image.format, save_all=True)\n    else:\n        image.save(full_file, format=image.format)\n    image.thumbnail((thumb_size, thumb_size))\n    image.save(thumb_file, format=image.format)\n    return dest\n"
  },
  {
    "path": "tedimg/static/empty",
    "content": ""
  },
  {
    "path": "tedimg/templates/banner.html",
    "content": "{% extends \"base.html\" %}\n\n{% block content %}\n<div class=\"section no-pad-bot\" id=\"index-banner\">\n  <div class=\"container\">\n    {% block banner_content %}\n    {% endblock %}\n    <br><br>\n  </div>\n</div>\n\n<div class=\"container\">\n  <div class=\"section\">\n    {% block section_content %}\n    {% endblock %}\n  </div>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "tedimg/templates/base.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1.0\"/>\n  <title>{{ config[\"SITE_NAME\"] }}</title>\n  <script src=\"/static/app.js\"></script>\n</head>\n<body>\n  <nav class=\"blue\" role=\"navigation\">\n    <div class=\"nav-wrapper container\">\n      <i class=\"material-icons left\">image</i><a href=\"/\" class=\"brand-logo\">{{ config[\"SITE_NAME\"] }}</a>\n    </div>\n  </nav>\n\n  {% block content %}\n  {% endblock %}\n\n  <footer class=\"page-footer blue\">\n    <div class=\"footer-copyright\">\n      <div class=\"container\">\n      <span class=\"right\"><i class=\"material-icons tiny\">call_split</i> on <a class=\"white-text\" href=\"https://github.com/kaiyou/tedimg\">Github</a>.</a>\n      </div>\n    </div>\n  </footer>\n  </body>\n</html>\n"
  },
  {
    "path": "tedimg/templates/error.html",
    "content": "{% extends \"base.html\" %}\n\n{% block content %}\n<div class=\"section no-pad-bot\" id=\"index-banner\">\n  <div class=\"container\">\n    <h1 class=\"red-text\">Upload failed!</h1>\n    <h2>{{ message }}</h2>\n    <br><br>\n  </div>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "tedimg/templates/index.html",
    "content": "{% extends \"banner.html\" %}\n\n{% block banner_content %}\n<h1 class=\"header center orange-text\">Upload your image!</h1>\n<form method=\"post\" id=\"upload\" action=\"{{ url_for('upload') }}\" enctype=\"multipart/form-data\">\n  <div class=\"file-field input-field\">\n    <div>\n      <i class=\"material-icons left small\">publish</i>\n      <input type=\"file\" name=\"file\" accept=\"image/*\">\n    </div>\n    <div class=\"file-path-wrapper\">\n      <input class=\"file-path validate\" type=\"text\" placeholder=\"Select your file\">\n    </div>\n  </div>\n\n  <div class=\"input-field\">\n      <i class=\"material-icons prefix\">public</i>\n      <input id=\"icon_prefix\" type=\"text\" class=\"validate\" name=\"url\" placeholder=\"Or paste your URL\">\n  </div>\n</form>\n{% endblock %}\n"
  },
  {
    "path": "tedimg/templates/show.html",
    "content": "{% extends \"banner.html\" %}\n\n{% block banner_content %}\n<h1 class=\"header center orange-text\">Upload successful!</h1>\n<div class=\"row center\">\n  <a href=\"{{ image }}\" class=\"btn-large blue\">Direct link</a>\n  <a href=\"{{ thumb }}\" class=\"btn-large blue\">Thumbnail</a>\n</div>\n<div class=\"row center\">\n  <img src=\"{{ thumb }}\">\n</div>\n{% endblock %}\n\n{% block section_content %}\n<div class=\"row\">\n  <div class=\"col s6\">\n    <h5 class=\"center\">Full size</h5>\n    <div class=\"input-field\">\n      <i class=\"material-icons prefix\">image</i>\n      <input class=\"snippet\" type=text value=\"{{ image }}\">\n    </div>\n    <div class=\"input-field\">\n      <i class=\"material-icons prefix\">code</i>\n      <input class=\"snippet\" type=text value=\"&lt;img src=&quot;{{ image }}&quot;&gt;\">\n    </div>\n    <div class=\"input-field\">\n      <i class=\"material-icons prefix\">chat</i>\n      <input class=\"snippet\" type=text value=\"[img]{{ image }}[/img]\">\n    </div>\n    <div class=\"input-field\">\n      <i class=\"material-icons prefix\">subject</i>\n      <input class=\"snippet\" type=text value=\"![Image]({{ image }})\">\n    </div>\n  </div>\n  <div class=\"col s6\">\n    <h5 class=\"center\">Thumbnail</h5>\n    <div class=\"input-field\">\n      <i class=\"material-icons prefix\">image</i>\n      <input class=\"snippet\" type=text value=\"{{ thumb }}\">\n    </div>\n    <div class=\"input-field\">\n      <i class=\"material-icons prefix\">code</i>\n      <input class=\"snippet\" type=text value=\"&lt;a href=&quot;{{ image }}&quot;&gt;&lt;img src=&quot;{{ thumb }}&quot;&gt;&lt;/a&gt;\">\n    </div>\n    <div class=\"input-field\">\n      <i class=\"material-icons prefix\">chat</i>\n      <input class=\"snippet\" type=text value=\"[url={{ image }}][img]{{ thumb }}[/img][/url]\">\n    </div>\n    <div class=\"input-field\">\n      <i class=\"material-icons prefix\">subject</i>\n      <input class=\"snippet\" type=text value=\"[![Image]({{ thumb }})]({{ image }})\">\n    </div>\n  </div>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "tedimg/views.py",
    "content": "from tedimg import app, images\n\nimport flask\nimport urllib\nimport os\n\n\n@app.route('/')\ndef index():\n    return flask.render_template(\"index.html\")\n\n\n@app.route('/show/<path:path>')\ndef show(path):\n    root = flask.url_for(\"index\", _external=True)\n    image, thumb = images.get_image(root, path)\n    return flask.render_template(\"show.html\", image=image, thumb=thumb)\n\n\n@app.route('/upload', methods=['POST'])\ndef upload():\n    url = flask.request.form.get('url')\n    uploaded = flask.request.files.get('file')\n    # Get an image object from the uploaded image or URL\n    try:\n        if uploaded:\n            image = images.image_from_file(uploaded)\n            filename = os.path.basename(uploaded.filename)\n        elif url:\n            image = images.image_from_url(url)\n            parsed = urllib.parse.urlparse(url)\n            filename = os.path.basename(parsed.path)\n        else:\n            return flask.render_template(\"error.html\", message=\"Missing image.\")\n    except Exception as error:\n        __import__(\"traceback\").print_exc()\n        return flask.render_template(\"error.html\", message=\"Could not store your image.\")\n    # Save the image to a local file\n    result = images.save_with_thumbnail(image, filename)\n    return flask.redirect(flask.url_for(\"show\", path=result))\n"
  },
  {
    "path": "webpack.config.js",
    "content": "var webpack = require(\"webpack\");\nvar path = require(\"path\");\n\nmodule.exports = {\n  mode: 'development',\n\n  entry: './resources/main.js',\n  output: {\n    filename: 'app.js',\n    path: path.resolve(__dirname, 'tedimg/static'),\n    publicPath: '/static/'\n  },\n\n  module: {\n    rules: [\n      {\n        test: /\\.css$/,\n        use: ['style-loader', 'css-loader', 'resolve-url-loader']\n      },\n      {\n        test: /\\.(png|woff2?)$/,\n        use: ['file-loader']\n      }\n    ]\n  }\n}\n"
  }
]