├── lib └── empty ├── .gitignore ├── requirements.txt ├── favicon.ico ├── img10.png ├── cron.yaml ├── screenshot ├── app.yaml ├── README.md ├── LICENSE ├── img10.html └── img10.py /lib/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | lib 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | GoogleAppEngineCloudStorageClient 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onur/img10/HEAD/favicon.ico -------------------------------------------------------------------------------- /img10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onur/img10/HEAD/img10.png -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: remove old images 3 | url: /tasks/remove 4 | schedule: every 3 hours 5 | -------------------------------------------------------------------------------- /screenshot: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if ! scrot -v 2>&1 > /dev/null; then 6 | exit 1 7 | fi 8 | 9 | TEMP_FILE=$(mktemp --suffix=.png) 10 | 11 | scrot "$@" $TEMP_FILE 12 | curl -s -F "img=@$TEMP_FILE" https://img10-1342.appspot.com/upload 13 | rm $TEMP_FILE 14 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | 5 | libraries: 6 | - name: webapp2 7 | version: latest 8 | - name: jinja2 9 | version: latest 10 | 11 | handlers: 12 | - url: /favicon\.ico 13 | static_files: favicon.ico 14 | upload: favicon\.ico 15 | - url: /img10\.png 16 | static_files: img10.png 17 | upload: img10\.png 18 | - url: /.* 19 | script: img10.app 20 | secure: always 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # img10 2 | 3 | Temporary image hosting service based on Google App Engine and Google Cloud 4 | Storage. 5 | 6 | ## Sharing Screenshots Temporarily 7 | 8 | You can use `screenshot` script to take screenshots with scrot and upload 9 | them to img10.xyz. 10 | 11 | ![screenshot sharing](https://i.imgur.com/VP3cU91.png "screenshot sharing") 12 | 13 | Or you can use curl to upload files directly with: 14 | 15 | ```sh 16 | curl -s -F "img=@myimage.png" https://img10.xyz/upload 17 | ``` 18 | 19 | ## Installation 20 | 21 | Clone repository, install requirements with pip and download 22 | [Google App Engine SDK](https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Python) 23 | and start the `dev_appserver.py` in repository directory. 24 | 25 | ```sh 26 | git clone https://github.com/onur/img10.git img10 27 | cd img10 28 | pip install -t lib -r requirements.txt 29 | $PATH_TO_APPENGINE_SDK/dev_appserver.py . 30 | ``` 31 | 32 | Your instance should be available in: `http://localhost:8080` 33 | 34 | 35 | ## TODO 36 | 37 | - [ ] Drag and drop support. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Onur Aslan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /img10.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | img10 - Temporary Image Hosting 7 | 8 | 9 | 10 | 11 | 35 | 36 | 37 |
38 |
39 |
40 |
41 |

Temporary Image Hosting

42 |
43 |
44 | {% if error %} 45 |

{{ error }}

46 | {% endif %} 47 | {% if not img_id %} 48 | Your image will be hosted for a day and will be removed. 49 | {% endif %} 50 | {% if img_id %} 51 | 52 | {% endif %} 53 |
54 |
55 | {% if img_url %} 56 |

{{ img_url }}

57 | {% endif %} 58 | 59 | 64 | 65 | 66 |
67 |
68 | 69 | 70 | 71 |
72 |
73 |
74 |
75 | 76 | 87 | 88 | -------------------------------------------------------------------------------- /img10.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (C) <2016> Onur Aslan 3 | # See COPYING for distribution information. 4 | 5 | import os 6 | from random import randint 7 | from google.appengine.api import images 8 | from google.appengine.api import app_identity 9 | from google.appengine.ext import ndb 10 | from google.appengine.ext import vendor 11 | import webapp2 12 | import jinja2 13 | import datetime 14 | 15 | vendor.add(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'lib')) 16 | import cloudstorage as gcs 17 | 18 | 19 | # Images removed after timeout (in seconds) 20 | TIMEOUT = 86400 21 | 22 | JINJA_ENVIRONMENT = jinja2.Environment( 23 | loader=jinja2.FileSystemLoader(os.path.dirname(__file__)), 24 | extensions=['jinja2.ext.autoescape'], 25 | autoescape=True) 26 | 27 | TEMPLATE = JINJA_ENVIRONMENT.get_template('img10.html') 28 | 29 | BUCKET_NAME = os.environ.get('BUCKET_NAME', 30 | app_identity.get_default_gcs_bucket_name()) 31 | 32 | 33 | ACME_VERIFICATION = "" 34 | 35 | 36 | def generate_id(size=7): 37 | chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 38 | id = list() 39 | for i in range(size): 40 | id.append(chars[randint(0, len(chars) - 1)]) 41 | return "".join(id) 42 | 43 | 44 | def remove_image(image_id): 45 | try: 46 | gcs.delete('/' + BUCKET_NAME + '/' + image_id) 47 | except: 48 | pass 49 | 50 | 51 | class Images(ndb.Model): 52 | """ Images model """ 53 | mime = ndb.StringProperty(required=True) 54 | date = ndb.DateTimeProperty(auto_now_add=True) 55 | thumbnail = ndb.BlobProperty(required=True) 56 | 57 | 58 | class Image(webapp2.RequestHandler): 59 | """ Image handler to serve images """ 60 | def get(self, image_id, extension): 61 | image = ndb.Key(Images, image_id).get() 62 | 63 | if image: 64 | delta = (datetime.datetime.now() - image.date).total_seconds() 65 | if delta >= TIMEOUT: 66 | self.not_found() 67 | return 68 | 69 | gcs_file = gcs.open('/' + BUCKET_NAME + '/' + image_id) 70 | content = gcs_file.read() 71 | gcs_file.close() 72 | 73 | if extension == 'jpg' and image.mime == 'image/png': 74 | orig_image = images.Image(content) 75 | orig_image.rotate(0) 76 | self.response.headers['Content-Type'] = 'image/jpeg' 77 | self.response.write( 78 | orig_image.execute_transforms( 79 | output_encoding=images.JPEG, quality=90)) 80 | else: 81 | self.response.headers['Content-Type'] = str(image.mime) 82 | self.response.write(content) 83 | return 84 | 85 | self.not_found() 86 | 87 | def not_found(self): 88 | self.response.set_status(404) 89 | self.response.write(TEMPLATE.render({ 90 | 'error': 'File not found!' 91 | })) 92 | 93 | 94 | class Thumbnail(webapp2.RequestHandler): 95 | """ Image handler to serve images """ 96 | def get(self, image_id): 97 | image = ndb.Key(Images, image_id).get() 98 | 99 | if image: 100 | self.response.headers['Content-Type'] = 'image/jpeg' 101 | self.response.write(image.thumbnail) 102 | return 103 | 104 | self.error(404) 105 | 106 | 107 | class Main(webapp2.RequestHandler): 108 | def get(self): 109 | self.response.write(TEMPLATE.render()) 110 | 111 | 112 | class RemoveOldImages(webapp2.RequestHandler): 113 | def get(self): 114 | images = Images.query( 115 | Images.date <= (datetime.datetime.now() - 116 | datetime.timedelta(seconds=TIMEOUT))) 117 | for image in images: 118 | remove_image(image.key.id()) 119 | image.key.delete() 120 | 121 | 122 | class Upload(webapp2.RequestHandler): 123 | """ Upload handler """ 124 | def post(self): 125 | img_from_request = self.request.get('img') 126 | image = images.Image(img_from_request) 127 | mime = "" 128 | extension = "" 129 | try: 130 | if image.format == images.JPEG: 131 | mime = "image/jpeg" 132 | extension = ".jpg" 133 | elif image.format == images.PNG: 134 | mime = "image/png" 135 | extension = ".png" 136 | except: 137 | self.response.set_status(500) 138 | self.response.write(TEMPLATE.render({ 139 | 'error': 'Unrecognized image format!' 140 | })) 141 | return 142 | 143 | id = generate_id() 144 | while ndb.Key(Images, id).get(): 145 | id = generate_id() 146 | 147 | write_retry_params = gcs.RetryParams(backoff_factor=1.1) 148 | gcs_file = gcs.open('/' + BUCKET_NAME + '/' + id, 149 | 'w', 150 | content_type=mime, 151 | retry_params=write_retry_params) 152 | gcs_file.write(img_from_request) 153 | gcs_file.close() 154 | 155 | thumb_img = images.Image(img_from_request) 156 | thumb_img.resize(width=492) 157 | thumbnail = thumb_img.execute_transforms(output_encoding=images.JPEG) 158 | 159 | img = Images(id=id, 160 | mime=mime, 161 | thumbnail=thumbnail) 162 | img_key = img.put() 163 | img_url = self.request.host_url + '/' + id + extension 164 | 165 | if self.request.headers.get('User-Agent').find('curl') >= 0: 166 | self.response.headers['Content-Type'] = 'text/plain' 167 | self.response.write(img_url + "\n") 168 | else: 169 | self.response.write(TEMPLATE.render({ 170 | 'img_id': img_key.id(), 171 | 'img_url': self.request.host_url + '/' + id + extension 172 | })) 173 | 174 | 175 | class LetsEncrypt(webapp2.RequestHandler): 176 | """ LetsEncrypt handler to verify ACME requests """ 177 | def get(self): 178 | self.response.headers['Content-Type'] = 'text/plain' 179 | self.response.write(ACME_VERIFICATION) 180 | 181 | 182 | app = webapp2.WSGIApplication([ 183 | ('/', Main), 184 | (r'/(\w+)\.(jpg|png)', Image), 185 | (r'/t/(\w+)\.jpg', Thumbnail), 186 | ('/upload', Upload), 187 | ('/tasks/remove', RemoveOldImages), 188 | ('/.well-known/acme-challenge/.*?', LetsEncrypt), 189 | ], debug=True) 190 | --------------------------------------------------------------------------------