├── .gitignore ├── INSTALL.md ├── LICENSE.txt ├── README.md ├── config ├── imsto.ini ├── nginx │ └── host.imsto.conf └── uwsgi │ ├── dev.ini │ └── prd.ini ├── imsto ├── __init__.py ├── _base.py ├── _config.py ├── _util.py ├── _wand.py ├── django │ ├── __init__.py │ ├── handle.py │ ├── storage.py │ ├── urls.py │ └── widgets.py ├── handlers.py ├── image.py ├── migrate.py ├── store.py ├── tool.py └── weedfs.py ├── sbin ├── server_image.sh ├── server_man.sh ├── setup_sto.sh └── uwsgi.init.gentoo.sh ├── setup.py ├── static └── admin │ ├── css │ ├── base.css │ ├── colorbox.css │ ├── images.css │ ├── images │ │ ├── controls.png │ │ └── loading.gif │ ├── pager.css │ └── upload.css │ ├── gallery.htm │ ├── index.htm │ ├── js │ ├── dust_func.js │ ├── jquery.colorbox-min.js │ ├── jquery.imgupload.js │ ├── jquery.pager.js │ └── jquery.upload.js │ └── upload.htm └── test ├── test_image.py ├── test_s3.py ├── test_thumb.py └── test_watermark.py /.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | /cache/* 3 | /dist/* 4 | /logs/* 5 | Thumbs.db 6 | *~ 7 | *.lock 8 | *.DS_Store 9 | ._* 10 | .Spotlight-V100 11 | .Trashes 12 | .*.sw[a-z] 13 | *.swp 14 | *.out 15 | *.class 16 | *.py[co] 17 | *.egg-info 18 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | 2 | Installation 3 | ------------ 4 | 5 | ### Gentoo Linux 6 | 7 | * add this line to your make.conf 8 | 9 | `USE_PYTHON="2.7 3.1 3.2"` 10 | 11 | * `sudo emerge -av imagemagick` 12 | * `sudo emerge -av dev-lang/python:2.7` 13 | * `sudo eselect python set python2.7` 14 | * `sudo emerge -av mongodb` 15 | * `sudo emerge -av dev-python/pip` 16 | * `sudo pip install pymongo` 17 | 18 | ### OSX 19 | 20 | * `sudo port install ImageMagick +no_x11` 21 | * `sudo port install python27` 22 | * `sudo port install mongodb` 23 | * `sudo port install py27-pymongo` 24 | 25 | 26 | ## Common: uWSGI 27 | 28 | * `wget http://projects.unbit.it/downloads/uwsgi-lts.tar.gz` 29 | * `tar zxvf uwsgi-lts.tar.gz` 30 | * `cd uwsgi-$VERSION` 31 | * `make` 32 | * `cp uwsgi /usr/local/bin/` 33 | 34 | ## ImSto 35 | 36 | * `cd /opt` 37 | * `git clone git://github.com/liut/imsto.git` 38 | * `cd imsto` 39 | * `less README.md`: read launch development 40 | 41 | 42 | Launch development 43 | ------------------ 44 | 45 | * mongodb: 46 | 47 | mongo localhost/storage 48 | 49 | db.createCollection("img.files",{autoIndexId:false}); 50 | db.img.files.ensureIndex({md5:1},{background:true, unique:true, dropDups:true}); 51 | 52 | * nginx: add config/nginx/host.imsto.conf to nginx.conf 53 | 54 | include /opt/imsto/config/nginx/host.imsto.conf; 55 | 56 | vim /etc/hosts 57 | 58 | 127.0.0.1 m.imsto.net man.imsto.net 59 | 60 | * uwsgi: there have two socket service 61 | 62 | start image handle: 63 | `sudo ./sbin/server_image.sh start` 64 | 65 | start manage handle: 66 | `./server_man.sh start` 67 | 68 | * open url http://man.imsto.net/ 69 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, "liut" 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of the imsto nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ImSto: a little image storage 2 | ============================= 3 | 4 | Requirements 5 | ------------ 6 | 7 | * MongoDB (GridFS) 8 | * Python 2.7 + [pymongo][pymongo] 9 | * [ImageMagick][ImageMagick] 10 | * Nginx + [uWSGI][uWSGI] 11 | * jpegoptim (option) 12 | 13 | 14 | 15 | TODO list 16 | --------- 17 | 18 | - store tool (closed 80%) 19 | - (web) gallery manager (need refactory) 20 | - (web) image upload (need refactory) 21 | - permisions & auth 22 | - demo: [demo] 23 | 24 | [pymongo]: http://pypi.python.org/pypi/pymongo/ 25 | [ImageMagick]: http://www.imagemagick.org/ 26 | [uWSGI]: http://projects.unbit.it/uwsgi/ 27 | [demo]: http://demo.imsto.org/ 28 | -------------------------------------------------------------------------------- /config/imsto.ini: -------------------------------------------------------------------------------- 1 | [common] 2 | servers = localhost 3 | ;replica_set = None, 4 | engine = mongodb 5 | db_name = storage 6 | fs_prefix = img 7 | thumb_path = /thumb 8 | thumb_root = /opt/imsto/cache/thumb/ 9 | temp_root = /tmp/ 10 | ;;thumb_method = shell, wand, pil 11 | thumb_method = wand 12 | url_prefix = http://m.imsto.net/ 13 | eggs_cache = /opt/imsto/cache/eggs 14 | max_file_size = 102400 15 | max_jpeg_quality = 88 16 | max_width = 1200 17 | max_height = 1200 18 | support_size = 120,160,250,400 19 | admin_name = imsto 20 | admin_pass = 21 | 22 | [imsto] 23 | servers = mongodb://localhost 24 | thumb_path = /t1 25 | thumb_root = /opt/imsto/cache/thumb/ 26 | temp_root = /opt/imsto/cache/temp/ 27 | ;thumb_method = shell, wand, pil 28 | thumb_method = wand 29 | max_file_size = 262114 30 | support_size = 120,130,160 31 | url_prefix = http://m.imsto.net/ 32 | admin_name = liutao 33 | admin_pass = 34 | force_jpeg = 1 35 | auto_scale = 1 36 | 37 | 38 | [s3] 39 | ; common s3 config 40 | ; S3_ACCESS_KEY and S3_SECRET_KEY can set into os environ, or set into wsgi_params 41 | bucket_name = liut 42 | 43 | engine = s3 44 | fs_prefix = s3 45 | thumb_path = /t2 46 | max_file_size = 152400 47 | force_jpeg = 1 48 | 49 | 50 | [weed] 51 | engine = weedfs 52 | fs_prefix = weed 53 | thumb_path = /t3 54 | -------------------------------------------------------------------------------- /config/nginx/host.imsto.conf: -------------------------------------------------------------------------------- 1 | 2 | ## $Id$ 3 | 4 | 5 | server { 6 | listen 80; 7 | server_name m.imsto.net; 8 | 9 | location / { 10 | index index.htm; 11 | root /opt/imsto/static; 12 | expires 1d; 13 | } 14 | 15 | location /t1/ { 16 | alias /opt/imsto/cache/thumb/; 17 | error_page 404 = @imsto_1; 18 | expires 7d; 19 | } 20 | 21 | location @imsto_1 { 22 | uwsgi_pass unix:///tmp/imsto.sock; 23 | uwsgi_param IMSTO_SECTION imsto; 24 | uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; 25 | include uwsgi_params; 26 | } 27 | 28 | location /t2/ { 29 | alias /opt/imsto/cache/thumb/; 30 | error_page 404 = @imsto_2; 31 | expires 7d; 32 | } 33 | 34 | location @imsto_2 { 35 | uwsgi_pass unix:///tmp/imsto.sock; 36 | uwsgi_param IMSTO_SECTION s3; 37 | uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; 38 | include uwsgi_params; 39 | } 40 | 41 | location /t3/ { 42 | alias /opt/imsto/cache/thumb/; 43 | error_page 404 = @imsto_3; 44 | expires 7d; 45 | } 46 | 47 | location @imsto_3 { 48 | uwsgi_pass unix:///tmp/imsto.sock; 49 | uwsgi_param IMSTO_SECTION weed; 50 | uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; 51 | include uwsgi_params; 52 | } 53 | 54 | # rewrite "^/([a-z0-9]{2})/([a-z0-9]{2})/([a-z0-9]{19,36})-([scwh]\d{2,4})\.(gif|jpg|jpeg|png)$" /t/$4/$1/$2/$3.$5 last; 55 | # rewrite "^/([a-z0-9]{2})/([a-z0-9]{2})/([a-z0-9]{19,36})\.(gif|jpg|jpeg|png)$" /t/orig/$1/$2/$3.$4 last; 56 | 57 | 58 | } 59 | 60 | server { 61 | listen 80; 62 | server_name man.imsto.net; 63 | root /opt/imsto/static/admin; 64 | index index.htm; 65 | 66 | location /upload.htm { 67 | add_header Content-Type "application/xhtml+xml"; 68 | 69 | } 70 | 71 | location /Manage { 72 | uwsgi_pass unix:///tmp/imsto_man.sock; 73 | uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; 74 | include uwsgi_params; 75 | } 76 | 77 | 78 | 79 | } -------------------------------------------------------------------------------- /config/uwsgi/dev.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket = /tmp/imsto.sock 3 | chmod-socket = 666 4 | daemonize = /opt/imsto/logs/img.log 5 | pidfile = /opt/imsto/logs/run.pid 6 | master = true 7 | processes = 2 8 | limit-as = 64 9 | max-requests = 40 10 | pythonpath = /opt/imsto 11 | chdir = /opt/imsto 12 | env = IMSTO_CONF_DIR=/opt/imsto/config 13 | module = imsto.handlers:ImageHandler 14 | harakiri = 30 15 | memory-debug = true 16 | memory-report = true 17 | vacuum = true 18 | 19 | [app_man] 20 | socket = /tmp/imsto_man.sock 21 | chmod-socket = 666 22 | daemonize = /opt/imsto/logs/img_man.log 23 | pidfile = /opt/imsto/logs/run_man.pid 24 | master = true 25 | processes = 2 26 | limit-as = 64 27 | max-requests = 40 28 | pythonpath = /opt/imsto/app 29 | module = AuthAdminHandle 30 | harakiri = 30 31 | memory-debug = true 32 | memory-report = true 33 | vacuum = true 34 | 35 | -------------------------------------------------------------------------------- /config/uwsgi/prd.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket = /tmp/imsto.sock 3 | chmod-socket = 666 4 | master = true 5 | processes = 5 6 | limit-as = 128 7 | max-requests = 4000 8 | pythonpath = /opt/imsto 9 | chdir = /opt/imsto 10 | env = IMSTO_CONF_DIR=/opt/imsto/config 11 | module = imsto.handlers:ImageHandler 12 | harakiri = 15 13 | optimize = 2 14 | disable-logging = true 15 | ;memory-debug = true 16 | ;memory-report = true 17 | 18 | 19 | -------------------------------------------------------------------------------- /imsto/__init__.py: -------------------------------------------------------------------------------- 1 | from _base import * 2 | from _config import * 3 | from _util import * 4 | from image import * 5 | from store import * 6 | 7 | #__all__ = (_base.__all__ + _config.__all__ + _util.__all__) 8 | 9 | 10 | VERSION = (1, 2, 4) 11 | 12 | 13 | def get_version(): 14 | if isinstance(VERSION[-1], basestring): 15 | return '.'.join(map(str, VERSION[:-1])) + VERSION[-1] 16 | return '.'.join(map(str, VERSION)) 17 | 18 | __version__ = get_version() 19 | __author__ = 'Eagle Liut' 20 | __author_email__ = 'liutao@liut.cc' 21 | -------------------------------------------------------------------------------- /imsto/_base.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ['base_convert'] 3 | 4 | #BASE2 = "01" 5 | #BASE10 = "0123456789" 6 | #BASE16 = "0123456789abcdef" 7 | #BASE36 = "0123456789abcdefghijklmnopqrstuvwxyz" 8 | BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 9 | 10 | 11 | def base_convert(number,frombase = 10, tobase = 36): 12 | """ converts a "number" between two bases of arbitrary digits 13 | 14 | The input number is assumed to be a string of digits from the 15 | fromdigits string (which is in order of smallest to largest 16 | digit). The return value is a string of elements from todigits 17 | (ordered in the same way). The input and output bases are 18 | determined from the lengths of the digit strings. Negative 19 | signs are passed through. 20 | 21 | 22 | """ 23 | 24 | if not 2 <= frombase <= 62 or not 2 <= tobase <= 62: 25 | raise ValueError, 'The base number must be between 2 and 36.' 26 | 27 | if str(number)[0]=='-': 28 | number = str(number)[1:] 29 | neg=1 30 | else: 31 | neg=0 32 | 33 | fromdigits = BASE62[:frombase] 34 | todigits = BASE62[:tobase] 35 | 36 | #print(fromdigits, todigits) 37 | #print(len(fromdigits), len(todigits)) 38 | 39 | # make an integer out of the number 40 | x=long(0) 41 | for digit in str(number): 42 | #print '%d %s %d' % (x, digit, fromdigits.index(digit)) 43 | x = x*frombase + fromdigits.index(digit) 44 | 45 | #print 'last x: %s' % x 46 | ## create the result in base 'len(todigits)' 47 | res="" 48 | while x>0: 49 | digit = x % tobase 50 | #print 'x %s d %s' % (x, digit) 51 | res = todigits[digit] + res 52 | x /= tobase 53 | if neg: 54 | res = "-"+res 55 | 56 | return res 57 | 58 | if __name__ == '__main__': 59 | a = 45 60 | print '{0}: {1}'.format(a, base_convert(a, 10, 36)) 61 | a = 'abcd' 62 | print '{0}: {1}'.format(a, base_convert(a, 16, 36)) 63 | a = '5cc163b92ab9b482b4486999d354f91e' 64 | print '{0}: {1}'.format(a, base_convert(a, 16, 36)) 65 | -------------------------------------------------------------------------------- /imsto/_config.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | ImSto Config 4 | 5 | Created by liut on 2010-12-15. 6 | Copyright (c) 2010-2013 liut. All rights reserved. 7 | """ 8 | 9 | __all__ = ['Config'] 10 | 11 | import ConfigParser,os 12 | 13 | class Singleton(type): 14 | _instances = {} 15 | def __call__(cls, *args, **kwargs): 16 | if cls not in cls._instances: 17 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 18 | return cls._instances[cls] 19 | 20 | class Config(object): 21 | __metaclass__ = Singleton 22 | 23 | """docstring for Config""" 24 | def __init__(self): 25 | 26 | self.defaulting = { 27 | 'servers': 'localhost', 28 | 'replica_set': None, 29 | 'engine': 'mongodb', 30 | 'db_name': 'storage', 31 | 'fs_prefix': 'img', 32 | 'thumb_path': '/thumb', 33 | 'thumb_root': '/opt/imsto/cache/thumb/', 34 | 'temp_root': '/tmp/', 35 | 'thumb_method': 'shell', # shell, wand, pil 36 | 'url_prefix': 'http://m.imsto.net/', 37 | 'eggs_cache': '/opt/imsto/cache/eggs', 38 | 'max_file_size': '102400', 39 | 'max_jpeg_quality': '88', 40 | 'max_width': '1920', 41 | 'max_height': '1920', 42 | 'support_size': '120,160,250,400', 43 | 'admin_name': 'imsto', 44 | 'admin_pass': '', 45 | } 46 | self.config = ConfigParser.ConfigParser() 47 | if os.environ.has_key('IMSTO_CONF_DIR'): 48 | ini_file = os.path.join(os.environ['IMSTO_CONF_DIR'], 'imsto.ini') 49 | else: 50 | ini_file = os.path.join(os.getcwd(), 'config/imsto.ini') 51 | print 'config: {}'.format(ini_file) 52 | 53 | ret = self.config.read(ini_file) 54 | if len(ret) == 0: 55 | print('Error: imsto.ini not found or read error') 56 | 57 | if os.environ.has_key('PYTHON_EGG_CACHE') and not (os.environ['PYTHON_EGG_CACHE'] is None): 58 | pass 59 | else: 60 | pass 61 | #os.environ['PYTHON_EGG_CACHE'] = self.get('eggs_cache') 62 | 63 | def get(self, name, section='imsto'): 64 | """docstring for get""" 65 | for s in [section, 'common']: 66 | if self.config.has_option(s, name): 67 | return self.config.get(s, name) 68 | 69 | if name.startswith('s3_'): 70 | if not name.upper() in os.environ: 71 | print 'ENV %s not found' % name.upper() 72 | return os.environ.get(name.upper(), None) 73 | 74 | if self.defaulting.has_key(name): 75 | return self.defaulting[name]; 76 | 77 | return None 78 | 79 | def sections(self): 80 | ss = self.config.sections() 81 | if 'common' in ss: 82 | del ss[ss.index('common')] 83 | return ss 84 | 85 | def has_section(self, section): 86 | return self.config.has_section(section) 87 | 88 | 89 | if __name__ == '__main__': 90 | config = Config() 91 | config2 = Config() 92 | print id(config) 93 | print id(config2) 94 | print(config.config.sections()) 95 | print(config.get('servers')) 96 | print config.get('thumb_root') 97 | print config.get('thumb_root', 'avatar') -------------------------------------------------------------------------------- /imsto/_util.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | _util.py 4 | 5 | Created by liut on 2010-12-04. 6 | Copyright (c) 2010-2013 liut. All rights reserved. 7 | """ 8 | 9 | import os 10 | 11 | from _wand import NewMagickWand,MagickReadImage,MagickToMime,\ 12 | MagickGetImageFormat,MagickGetImageWidth,MagickGetImageHeight,MagickGetImageCompressionQuality 13 | 14 | __all__ = [ 15 | 'check_dirs', 'save_file', 'identify_image', 'guessImageType', 16 | 'identify_image', 'thumb_image', 'watermark_image', 17 | 'guess_mimetype', 'guess_ext', 'password_hash', 18 | 'encode_upload', 'jpegoptim' 19 | ] 20 | 21 | def check_dirs(filename): 22 | dir_name = os.path.dirname(filename) 23 | if not os.path.exists(dir_name): 24 | os.makedirs(dir_name, 0777) 25 | 26 | def save_file(filename, file = None, blob = None): 27 | check_dirs(filename) 28 | if file is None and blob is None: 29 | raise ValueError('invalid argument: file and blob are both None') 30 | fp = open(filename, 'wb') 31 | try: 32 | fp.write(blob if blob else file.read()) 33 | except Exception, e: 34 | print('save file {} failed, error: {}'.format(filename, e)) 35 | finally: 36 | fp.close() 37 | 38 | statinfo = os.stat(filename) 39 | if statinfo.st_size == 0: 40 | print('file size is zero, remove it') 41 | os.remove(filename) 42 | 43 | sig_gif = b'GIF' 44 | sig_jpg = b'\xff\xd8\xff' 45 | #sig_png = b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a' 46 | sig_png = b"\211PNG\r\n\032\n" 47 | 48 | def guessImageType(data): 49 | if data[:3] == sig_gif: 50 | return 'gif' 51 | elif data[:3] == sig_jpg: 52 | return 'jpg' 53 | elif data[:8] == sig_png: 54 | return 'png' 55 | else: 56 | return None 57 | 58 | """ 59 | test log 60 | magickwand: 25M 61 | PIL: 12M 62 | shell: 11M 63 | """ 64 | def thumbnail_shell(filename, size_1, distname): 65 | size = size_1, size_1 66 | info = identify_shell(filename) 67 | if info is None: 68 | return None 69 | if info['size'] > size: 70 | print('thumbnail {0} to: {1}'.format(filename, size_1)) 71 | from subprocess import check_call 72 | check_call(['convert','-thumbnail',str(size_1),filename,distname]) 73 | else: 74 | from shutil import copyfile 75 | copyfile(filename, distname) 76 | 77 | def identify_shell(imagefile): 78 | from subprocess import check_output 79 | try: 80 | output = check_output(['identify', '-format', '%m %w %h %Q', imagefile]) 81 | info = output.split(' ') 82 | return {'format': info[0], 'size': (int(info[1]), int(info[2])), 'quality': int(info[3])} 83 | except CalledProcessError, e: 84 | print (e) 85 | return None 86 | 87 | def identify_wand(imagefile): 88 | magick_wand = NewMagickWand() 89 | if not MagickReadImage(magick_wand, imagefile): 90 | return False 91 | format = MagickGetImageFormat(magick_wand) 92 | info = { 93 | 'format': format, 94 | 'mime': MagickToMime(format), 95 | 'size': (MagickGetImageWidth(magick_wand), MagickGetImageHeight(magick_wand)), 96 | 'quality': MagickGetImageCompressionQuality(magick_wand) 97 | } 98 | 99 | DestroyMagickWand(magick_wand) 100 | 101 | return info 102 | 103 | def identify_image(imagefile, method = 'wand'): 104 | if method == 'wand': 105 | return identify_wand(imagefile) 106 | if method == 'shell': 107 | return identify_shell(imagefile) 108 | 109 | def thumbnail_wand(filename, width, height, distname, mode='s'): 110 | from image import SimpImage 111 | im = SimpImage(filename) 112 | if mode == 'c': 113 | ret = im.cropThumbnail(width, height) 114 | elif mode == 'w': 115 | ret = im.thumbnail(width, max_width=width) 116 | elif mode == 'h': 117 | ret = im.thumbnail(width, max_height=width) 118 | else: 119 | ret = im.thumbnail(width, height) 120 | 121 | print "thumbnail {} {}x{} result: {}".format(mode, width, height, ret) 122 | if ret: 123 | ret = im.save(distname) 124 | del im 125 | 126 | return ret 127 | 128 | 129 | 130 | def thumbnail_pil(filename, size_1, distname): 131 | size = size_1, size_1 132 | from PIL import Image 133 | im = Image.open(filename) 134 | if im.size > size: 135 | im.thumbnail(size, Image.ANTIALIAS) 136 | im.save(distname, im.format) 137 | del im 138 | 139 | def thumb_image(filename, width, height, distname, mode='s', method='wand'): 140 | check_dirs(distname) 141 | if method == 'shell': 142 | return thumbnail_shell(filename, width, distname) 143 | elif method == 'wand': 144 | return thumbnail_wand(filename, width, height, distname, mode=mode) 145 | elif method == 'pil': 146 | return thumbnail_pil(filename, width, distname) 147 | 148 | 149 | 150 | def guess_mimetype(fn, default="application/octet-stream"): 151 | """Guess a mimetype from filename *fn*. 152 | 153 | >>> guess_mimetype("foo.txt") 154 | 'text/plain' 155 | >>> guess_mimetype("foo") 156 | 'application/octet-stream' 157 | """ 158 | import mimetypes 159 | if "." not in fn: 160 | return default 161 | bfn, ext = fn.lower().rsplit(".", 1) 162 | if ext == "jpg": ext = "jpeg" 163 | return mimetypes.guess_type(bfn + "." + ext)[0] or default 164 | 165 | def guess_ext(mime): 166 | import mimetypes 167 | ext = mimetypes.guess_extension(mime) 168 | if ext == '.jpe': 169 | return '.jpg' 170 | return ext 171 | 172 | def watermark_image(filename, distname): 173 | from image import SimpImage 174 | im = SimpImage(filename) 175 | if os.environ.has_key('IMSTO_CONF_DIR'): 176 | watermark = os.path.join(os.environ['IMSTO_CONF_DIR'], 'watermark.png') 177 | copyright = os.path.join(os.environ['IMSTO_CONF_DIR'], 'watermark-copy.png') 178 | else: 179 | watermark = os.path.join(os.getcwd(), 'config/watermark.png') 180 | copyright = os.path.join(os.getcwd(), 'config/watermark-copy.png') 181 | #print ini_file 182 | im_w = SimpImage(watermark) 183 | #print im_w.wand 184 | check_dirs(distname) 185 | ci = SimpImage(copyright) if os.access(copyright, os.R_OK) else None 186 | r = None 187 | if im.watermark(im_w, 0.5, position='golden', copyright=ci): 188 | print 'watermark ok' 189 | r = im.save(distname) 190 | 191 | if r is None: 192 | print 'error watermark' 193 | 194 | del im 195 | del im_w 196 | return r 197 | 198 | def password_hash(username, password): 199 | from hashlib import sha1 200 | return sha1(':'.join([username.lower(), password])).hexdigest() 201 | 202 | 203 | def encode_upload(file=None, content=None, content_type=None, name=None, ext_data=[]): 204 | """encode a upload file form 205 | Learn from: http://mancoosi.org/~abate/upload-file-using-httplib 206 | """ 207 | BOUNDARY = '----------bundary------' 208 | CRLF = '\r\n' 209 | #print CRLF 210 | body = [] 211 | # Add the metadata about the upload first 212 | for key, value in ext_data: 213 | body.extend( 214 | ['--' + BOUNDARY, 215 | 'Content-Disposition: form-data; name="%s"' % key, 216 | '', 217 | value, 218 | ]) 219 | # Now add the file itself 220 | if content is None: 221 | if file is None: 222 | raise ValueError('need file or content argument') 223 | if hasattr(file, 'read'): 224 | content = file.read() 225 | else: 226 | name = os.path.basename(file) 227 | f = open(file, 'rb') 228 | content = f.read() 229 | f.close() 230 | 231 | #print 'type content: %s, len content: %s' % (type(content), len(content)) 232 | 233 | if name is None: 234 | ext = guessImageType(content[:32]) 235 | name = 'data.{}'.format(ext) 236 | 237 | if content_type is None: 238 | content_type = guess_mimetype(name) 239 | 240 | body.extend( 241 | ['--' + BOUNDARY, 242 | str('Content-Disposition: form-data; name="file"; filename="%s"' % name), 243 | # The upload server determines the mime-type, no need to set it. 244 | str('Content-Type: %s' % content_type), 245 | '', 246 | content, 247 | ]) 248 | # Finalize the form body 249 | body.extend(['--' + BOUNDARY + '--', '']) 250 | return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body) 251 | 252 | def jpegoptim(imagefile, max_quality = 88): 253 | print 'jpegoptim {} (max {})'.format(imagefile, max_quality) 254 | import subprocess 255 | r = subprocess.call(["jpegoptim","--strip-all", "--max={}".format(max_quality), imagefile]) 256 | if r != 0: 257 | print 'call jpegoptim failed {}'.format(r) 258 | return False 259 | return True 260 | 261 | 262 | -------------------------------------------------------------------------------- /imsto/django/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from handle import ImageHandle, ManageHandle 3 | from storage import ImageStorage 4 | from widgets import AdminImageWidget 5 | -------------------------------------------------------------------------------- /imsto/django/handle.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | from imsto import load_imsto, UrlError, guess_mimetype 4 | from django.http import HttpResponse, HttpResponseNotFound, StreamingHttpResponse 5 | from django.conf import settings 6 | 7 | __all__ = ['ImageHandle', 'ManageHandle'] 8 | 9 | def ImageHandle(request, path): 10 | environ = request.META 11 | SECTION = get_section(request.META) 12 | 13 | imsto = load_imsto(SECTION) 14 | try: 15 | dst_file, dst_path = imsto.load(path) 16 | except UrlError, e: 17 | return HttpResponseNotFound(str(e)) 18 | except Exception, e: 19 | raise e 20 | finally: 21 | imsto.close() 22 | 23 | THUMB_PATH = imsto.get_config('thumb_path').rstrip('/') 24 | mimetype = guess_mimetype(dst_path) 25 | server_soft = environ.get('SERVER_SOFTWARE','') 26 | if server_soft[:5] == 'nginx' and os.name != 'nt': 27 | print('path: {0}/{1}'.format(THUMB_PATH, dst_path)) 28 | response = HttpResponse(content_type=mimetype) 29 | response['X-Accel-Redirect'] = '{0}/{1}'.format(THUMB_PATH, dst_path) 30 | return response 31 | 32 | fd = open(dst_file,'r') 33 | response = StreamingHttpResponse(fd, content_type=mimetype) 34 | response['Content-Length'] = os.path.getsize(dst_file) 35 | response['Via'] = 'imsto' 36 | return response 37 | #return environ['wsgi.file_wrapper'](fd, 4096) 38 | 39 | def ManageHandle(request, path): 40 | #print request 41 | #print path 42 | # TODO: 43 | return HttpResponse('manage') 44 | 45 | def get_section(environ): 46 | if hasattr(settings, 'IMSTO_SECTION'): 47 | return settings.IMSTO_SECTION 48 | elif environ.has_key('IMSTO_SECTION'): 49 | return environ.get('IMSTO_SECTION') 50 | else: 51 | return 'imsto' 52 | -------------------------------------------------------------------------------- /imsto/django/storage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import itertools 3 | from django.core.files.storage import Storage 4 | from django.conf import settings 5 | from imsto import load_imsto 6 | from urlparse import urljoin 7 | 8 | MIN_PATH_LEN = 28 9 | 10 | class ImageStorage(Storage): 11 | """A custom storage backend to store files in GridFS 12 | 13 | to use this backend, change your settings.py: 14 | 15 | DEFAULT_FILE_STORAGE = 'imsto.django.ImageStorage' 16 | 17 | """ 18 | 19 | def __init__(self, base_url=None): 20 | 21 | if base_url is None: 22 | base_url = settings.MEDIA_URL 23 | self.base_url = base_url 24 | self.imsto = load_imsto() 25 | self.field = 'image_path' 26 | 27 | def delete(self, name): 28 | """Deletes the specified file from the storage system. 29 | TODO: 30 | """ 31 | pass 32 | 33 | def exists(self, name): 34 | """Returns True if a file referened by the given name already exists in the 35 | storage system, or False if the name is available for a new file. 36 | """ 37 | image = self.imsto.exists(filename=name) 38 | return bool(image.name) 39 | 40 | def listdir(self, path=None): 41 | """Lists the contents of the specified path, returning a 2-tuple of lists; 42 | the first item being directories, the second item being files. 43 | """ 44 | return self.imsto.browse(limit=20,start=0) 45 | 46 | def size(self, name): 47 | """Returns the total size, in bytes, of the file specified by name. 48 | """ 49 | img = self.imsto.get(filename=name) 50 | if img: 51 | return img.length 52 | else: 53 | raise ValueError("No such file or directory: '%s'" % name) 54 | 55 | def url(self, name, size='orig'): 56 | """Returns an absolute URL where the file's contents can be accessed 57 | directly by a web browser. 58 | """ 59 | if len(name) > MIN_PATH_LEN and name[2] == name[5] == '/': 60 | return self.imsto.url(name, size) 61 | return urljoin(self.base_url, name).replace('\\', '/') 62 | 63 | def _open(self, name, mode='rb'): 64 | img = self.imsto.get(filename=name) 65 | if img: 66 | return img 67 | else: 68 | raise ValueError("No file found with the name '%s'." % name) 69 | 70 | def get_available_name(self, name): 71 | """Returns a filename that's free on the target storage system, and 72 | available for new content to be written to. 73 | """ 74 | print 'src name: %s' % name 75 | return os.path.basename(name) 76 | 77 | 78 | def _save(self, name, content): 79 | print 'available name: %s' % name 80 | print 'type of content: %s' % type(content) 81 | if hasattr(content, 'temporary_file_path'): 82 | file = content.temporary_file_path() 83 | print 'temp file: %s' % file 84 | r, id, filename = self.imsto.store(content.file,name=name) 85 | 86 | if r: 87 | print 'stored {}, {}, {}'.format(r, id, filename) 88 | return filename 89 | 90 | print 'store failed, name: %s' % name 91 | return None 92 | 93 | -------------------------------------------------------------------------------- /imsto/django/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from handle import ImageHandle, ManageHandle 4 | 5 | urlpatterns = patterns('', 6 | url(r'^t/(.+)$', ImageHandle), 7 | url(r'^Manage/(.*)$', ManageHandle), 8 | ) 9 | -------------------------------------------------------------------------------- /imsto/django/widgets.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.widgets import AdminFileWidget 2 | from django.utils.translation import ugettext as _ 3 | from django.utils.safestring import mark_safe 4 | 5 | class AdminImageWidget(AdminFileWidget): 6 | def render(self, name, value, attrs=None): 7 | output = [] 8 | print 'value: {} type {}'.format(value, type(value)) 9 | if value and getattr(value, "url", None): 10 | image_url = value.url 11 | file_name=str(value) 12 | output.append(u' %s %s ' % \ 13 | (image_url, image_url, file_name, _('Change:'))) 14 | output.append(super(AdminFileWidget, self).render(name, value, attrs)) 15 | return mark_safe(u''.join(output)) 16 | -------------------------------------------------------------------------------- /imsto/handlers.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | handlers.py 4 | 5 | Created by liut on 2010-12-04. 6 | Copyright (c) 2010-2013 liut. All rights reserved. 7 | """ 8 | 9 | import os,re 10 | import json 11 | from sys import exc_info 12 | from traceback import format_tb 13 | from imsto import * 14 | 15 | __all__ = [ 16 | 'ErrorWrap','AuthWrap', 17 | 'AuthAdminHandle','ImageHandler','AdminHandler' 18 | ] 19 | 20 | def abuilding(self, environ, start_response): 21 | """show abuilding""" 22 | start_response('200 OK', [('Content-type', 'text/plain')]) 23 | return ['Abuilding'] 24 | 25 | def not_found(environ, start_response, message = 'Not Found'): 26 | """Called if no URL matches.""" 27 | start_response('404 NOT FOUND', [('Content-Type', 'text/plain')]) 28 | return [message] 29 | 30 | def redirect(environ, start_response, redirect_to, message = 'Found'): 31 | start_response('302 Found', [('Location', redirect_to)]) 32 | return [] 33 | 34 | def print_env(environ, start_response): 35 | """list environ items""" 36 | start_response('200 OK', [('Content-Type', 'text/plain')]) 37 | return ['\n'.join(['%s: %r' % item for item in environ.items()])] 38 | 39 | def get_path_info(environ): 40 | if environ.has_key('app.url_args'): 41 | path = ''.join(environ.get('app.url_args')) 42 | else: 43 | path = environ.get('PATH_INFO', '') 44 | 45 | #print 'path: %s (%s)' % (path, type(path)) 46 | return path 47 | 48 | class ErrorWrap(object): 49 | """The middleware we use.""" 50 | def __init__(self, app): 51 | self.app = app 52 | 53 | def __call__(self, environ, start_response): 54 | """Call the application can catch exceptions.""" 55 | appiter = None 56 | try: 57 | appiter = self.app(environ, start_response) 58 | for item in appiter: 59 | yield item 60 | except: 61 | traceback = get_traceback() 62 | try: 63 | start_response('500 INTERNAL SERVER ERROR', [ 64 | ('Content-Type', 'text/plain')]) 65 | except: 66 | pass 67 | yield '\n'.join(traceback) 68 | 69 | if hasattr(appiter, 'close'): 70 | appiter.close() 71 | 72 | def get_traceback(): 73 | e_type, e_value, tb = exc_info() 74 | traceback = ['Traceback (most recent call last):'] 75 | traceback += format_tb(tb) 76 | traceback.append('%s: %s' % (e_type.__name__, e_value)) 77 | return traceback 78 | 79 | class AuthWrap(object): 80 | 81 | def __init__(self, app): 82 | self.app = app 83 | self.section = 'imsto' 84 | self.config = Config() 85 | 86 | def __call__(self, environ, start_response): 87 | self.section = environ.get('IMSTO_SECTION', 'imsto') 88 | if not self.authorized(environ.get('HTTP_AUTHORIZATION')): 89 | return self.auth_required(environ, start_response) 90 | return self.app(environ, start_response) 91 | 92 | def authorized(self, auth_header): 93 | if not auth_header: 94 | # If they didn't give a header, they better login... 95 | return False 96 | # .split(None, 1) means split in two parts on whitespace: 97 | auth_type, encoded_info = auth_header.split(None, 1) 98 | #print '{}, {}'.format(auth_type, encoded_info) 99 | assert auth_type.lower() == 'basic' 100 | unencoded_info = encoded_info.decode('base64') 101 | username, password = unencoded_info.split(':', 1) 102 | return self.check_password(username, password) 103 | 104 | def check_password(self, username, password): 105 | # Not very high security authentication... 106 | admin_name = self.config.get('admin_name') 107 | admin_pass = self.config.get('admin_pass') 108 | #print 'input %s:%s' % (username, password) 109 | if username != admin_name: 110 | return False 111 | if admin_pass == '': 112 | print 'admin_pass is empty!' 113 | return True 114 | 115 | #hashed = password_hash(username, password) 116 | #print '{} admin_pass\n{} hashed'.format(admin_pass, hashed) 117 | return password_hash(username, password) == admin_pass 118 | 119 | def auth_required(self, environ, start_response): 120 | start_response('401 Authentication Required', 121 | [('Content-type', 'text/html'), 122 | ('WWW-Authenticate', 'Basic realm="imsto"')]) 123 | return [""" 124 | 125 | Authentication Required 126 | 127 |

Authentication Required

128 | If you can't get in, then stay out. 129 | 130 | """] 131 | 132 | 133 | 134 | def ImageHandler(environ, start_response): 135 | """main image url handler""" 136 | SECTION = environ.get('IMSTO_SECTION', 'imsto') 137 | print 'section from env: {0}'.format(SECTION) 138 | imsto = load_imsto(SECTION) 139 | path = get_path_info(environ) 140 | #print 'path: %s' % path 141 | try: 142 | dst_file, dst_path = imsto.load(path) 143 | except HttpFound, e: 144 | return redirect(environ, start_response, e.path, e.message) 145 | except UrlError, e: 146 | return not_found(environ, start_response, e.message) 147 | except Exception, e: 148 | raise 149 | finally: 150 | imsto.close() 151 | 152 | print('dst_path: {}'.format(dst_path)) 153 | #print('dst_file: {}'.format(dst_file)) 154 | 155 | THUMB_PATH = imsto.get_config('thumb_path').rstrip('/') 156 | server_soft = environ.get('SERVER_SOFTWARE','') 157 | if server_soft[:5] == 'nginx' and os.name != 'nt': 158 | print('path: {0}/{1}'.format(THUMB_PATH, dst_path)) 159 | start_response('200 OK', [('X-Accel-Redirect', '{0}/{1}'.format(THUMB_PATH, dst_path))]) 160 | return [] 161 | #print(file.type) 162 | mimetype = guess_mimetype(dst_path) 163 | filesize = os.path.getsize(dst_file) 164 | headers = [('Content-Type', str(mimetype)), ('Content-Length', '{0}'.format(filesize)), ('Via','imsto')] 165 | #print(headers) 166 | start_response('200 OK', headers) 167 | fd = open(dst_file,'r') 168 | return environ['wsgi.file_wrapper'](fd, 4096) 169 | 170 | def AdminHandler(environ, start_response): 171 | path = get_path_info(environ) 172 | 173 | man_regex = r'(env|Gallery|Stored|Sections)$' 174 | match = re.search(man_regex, path) 175 | #print('match: {0}'.format(match)) 176 | if match is None: 177 | return not_found(environ, start_response) 178 | 179 | from cgi import FieldStorage 180 | form = FieldStorage(environ=environ) 181 | section = form.getfirst('roof', 'imsto') 182 | action, = match.groups() 183 | if (action == 'Gallery'): 184 | limit = 20 185 | start = 0 186 | if form.has_key("page") and form["page"].value != "": 187 | page = int(form["page"].value) 188 | if page < 1: 189 | page = 1 190 | start = limit * (page - 1) 191 | 192 | start_response('200 OK', [('Content-type', 'text/javascript')]) 193 | 194 | imsto = load_imsto(section) 195 | gallery = imsto.browse(limit, start) 196 | import datetime 197 | dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) else None 198 | if hasattr(imsto, 'close'): 199 | imsto.close() 200 | return [json.dumps(gallery, default=dthandler)] 201 | elif (action == 'Stored'): 202 | return StoredHandler(environ, start_response) 203 | #start_response('200 OK', [('Content-type', 'text/plain')]) 204 | #return ['Stored'] 205 | elif (action == 'env'): 206 | return print_env(environ, start_response) 207 | elif action == 'Sections': 208 | start_response('200 OK', [('Content-type', 'text/javascript')]) 209 | config = Config() 210 | return [json.dumps(config.sections())] 211 | 212 | start_response('200 OK', [('Content-type', 'text/plain')]) 213 | return [path_info] 214 | 215 | 216 | def StoredHandler(environ, start_response): 217 | from cgi import FieldStorage 218 | import cgitb; cgitb.enable(display=0, logdir="/tmp") 219 | form = FieldStorage(fp=environ['wsgi.input'], environ=environ) 220 | print(form.keys()) 221 | 222 | start_response('200 Ok', [('Content-type', 'text/javascript')]) 223 | 224 | if "oper" not in form: 225 | #print("Bad Request") 226 | return [json.dumps([False, 'Bad Request'])] 227 | 228 | method = environ['REQUEST_METHOD'].upper() 229 | if method == 'GET' or method == 'HEAD': 230 | return [json.dumps([False, 'bad request'])] 231 | oper = form['oper'] 232 | print(oper) 233 | section = form.getfirst('roof', 'imsto') 234 | # section = form['section'] if form.has_key('section') else 'imsto' 235 | 236 | imsto = load_imsto(section) 237 | if oper.value == 'delete': 238 | id = form['id'] 239 | r = imsto.delete(id.value) 240 | print r 241 | return [json.dumps(r)] 242 | if oper.value == 'add': 243 | 244 | if "new_file" not in form: 245 | return [json.dumps([False, 'please select a file'])] 246 | 247 | new_file = form['new_file'] 248 | if new_file is None: 249 | return [json.dumps([False, 'invalid upload field'])] 250 | # print(type(new_file)) 251 | result = [] 252 | if type(new_file) == type([]): 253 | for f in new_file: 254 | print('%r %r %r %r %r %r' % (f.name, f.filename, f.type, f.disposition, f.file, f.length)) 255 | r = imsto.store(f.file, ctype=f.type, name=f.filename) 256 | print 'store: %r, result %r' % (f.name, r) 257 | if type(r) == type([]): 258 | result.append(r) 259 | else: 260 | result.append(False) 261 | else: 262 | f = new_file 263 | print('single file %r %r' % (f.name, f.filename)) 264 | try: 265 | result = imsto.store(f.file, ctype=f.type, name=f.filename) 266 | print 'store: %r, result %r' % (f.name, result) 267 | except Exception, e: 268 | result = [False, e.message] 269 | print "\n".join(get_traceback()) + "\n" 270 | 271 | if hasattr(imsto, 'close'): 272 | imsto.close() 273 | 274 | return [json.dumps(result)] 275 | else: 276 | return [json.dumps([False, 'invalid operation'])] 277 | 278 | 279 | AuthAdminHandle = ErrorWrap(AuthWrap(AdminHandler)) 280 | 281 | 282 | # map urls to functions 283 | default_urls = [ 284 | (r't\d?/(.+)$', ImageHandler), 285 | (r'Manage/(.*)$', AuthWrap(AdminHandler)) 286 | ] 287 | 288 | def application(environ, start_response): 289 | """ 290 | The main WSGI application. Dispatch the current request to 291 | the functions from above and store the regular expression 292 | captures in the WSGI environment as `app.url_args` so that 293 | the functions from above can access the url placeholders. 294 | 295 | If nothing matches call the `not_found` function. 296 | """ 297 | import re 298 | path = environ.get('PATH_INFO', '').strip('/') 299 | for regex, callback in default_urls: 300 | match = re.search(regex, path) 301 | if match is not None: 302 | environ['app.url_args'] = match.groups() 303 | return callback(environ, start_response) 304 | return not_found(environ, start_response) 305 | 306 | 307 | 308 | if __name__ == '__main__': 309 | from wsgiref.simple_server import make_server 310 | httpd = make_server('', 8000, application) 311 | print("Listening on port 8000....\n image url example: http://localhost:8000/aj/3f/1ow9y7ks8w8s888kswkg8.jpg\n") 312 | httpd.serve_forever() 313 | 314 | else: 315 | application = ErrorWrap(application) 316 | 317 | 318 | 319 | -------------------------------------------------------------------------------- /imsto/image.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | image.py 4 | 5 | Created by liut on 2010-12-04. 6 | Copyright (c) 2010-2013 liut. All rights reserved. 7 | """ 8 | 9 | __all__ = ['SimpImage', 'MIN_QUALITY'] 10 | 11 | import ctypes,collections 12 | from _wand import (NewMagickWand,DestroyMagickWand,CloneMagickWand,ClearMagickWand, 13 | MagickReadImageBlob,MagickReadImage,MagickWriteImage,MagickGetImageBlob, 14 | MagickGetImageFormat,MagickSetImageFormat,MagickGetImageWidth,MagickGetImageHeight, 15 | MagickGetImageCompressionQuality,MagickSetImageCompressionQuality, 16 | MagickScaleImage,MagickRelinquishMemory,MagickStripImage,MagickThumbnailImage,MagickCropImage,MagickSetImagePage, 17 | MagickSetImageArtifact, 18 | BlendCompositeOp,DissolveCompositeOp,ModulateCompositeOp, 19 | MagickCompositeImage,MagickLabelImage, 20 | MagickSetImageGravity,CenterGravity,SouthGravity, 21 | MagickGetException,MagickClearException, 22 | ) 23 | import warnings 24 | 25 | import os 26 | 27 | # FORMAT_JPEG = 'JPEG' 28 | # FORMAT_PNG = 'PNG' 29 | # FORMAT_GIF = 'GIF' 30 | 31 | MIN_QUALITY = 72 32 | 33 | class SimpImage(object): 34 | _max_width, _max_height = 0, 0 35 | 36 | """docstring for ClassName""" 37 | def __init__(self, file = None, image=None, blob=None): 38 | if isinstance(image, SimpImage): 39 | self._wand = CloneMagickWand(image.wand) 40 | elif blob is not None: 41 | if not isinstance(blob, collections.Iterable): 42 | raise TypeError( 'blob must be iterable, not {}'.format(repr(blob)) ) 43 | if not isinstance(blob, basestring): 44 | blob = ''.join(blob) 45 | elif not isinstance(blob, str): 46 | blob = str(blob) 47 | self._wand = NewMagickWand() 48 | r = MagickReadImageBlob( self._wand, blob, len( blob ) ) 49 | if not r: 50 | self.error() 51 | else: 52 | self._wand = NewMagickWand() 53 | self.read(file) 54 | 55 | def __del__(self): 56 | if self._wand: 57 | self._wand = DestroyMagickWand( self._wand ) 58 | 59 | 60 | def __copy__( self ): 61 | return self.clone() 62 | 63 | def clone( self ): 64 | return type(self)(image=self) 65 | 66 | 67 | def _clear( self ): 68 | ClearMagickWand( self._wand ) 69 | 70 | 71 | def read( self, file): 72 | self._clear() 73 | 74 | if isinstance(file, basestring): 75 | if os.access(file, os.R_OK): 76 | r = MagickReadImage( self._wand, file ) 77 | else: 78 | #print 'image {} not found or access deny'.format(file) 79 | raise IOError('image {} not found or access deny'.format(file)) 80 | elif hasattr( file, 'read' ): 81 | c = file.read() 82 | r = MagickReadImageBlob( self._wand, c, len( c ) ) 83 | else: 84 | raise TypeError('file must be a readable file path or filelike object') 85 | 86 | if not r: 87 | self.error() 88 | 89 | 90 | @property 91 | def wand(self): 92 | return self._wand 93 | 94 | @property 95 | def format( self ): 96 | format = MagickGetImageFormat( self._wand ) 97 | if format == '': 98 | return None 99 | else: 100 | return format 101 | 102 | @format.setter 103 | def format(self, value): 104 | '''The image format as a string, eg. "PNG".''' 105 | MagickSetImageFormat( self._wand, value ) 106 | 107 | def max_height(): 108 | doc = "The max_height property." 109 | def fget(self): 110 | return self._max_height 111 | def fset(self, value): 112 | self._max_height = value 113 | def fdel(self): 114 | del self._max_height 115 | return locals() 116 | max_height = property(**max_height()) 117 | 118 | def max_width(): 119 | doc = "The max_width property." 120 | def fget(self): 121 | #if self._max_width is None: 122 | # self._max_width = 0 123 | return self._max_width 124 | def fset(self, value): 125 | self._max_width = value 126 | def fdel(self): 127 | del self._max_width 128 | return locals() 129 | max_width = property(**max_width()) 130 | 131 | @property 132 | def quality(self): 133 | return MagickGetImageCompressionQuality( self._wand ) 134 | 135 | @quality.setter 136 | def quality(self, value): 137 | MagickSetImageCompressionQuality( self._wand, int( round( value, 0 ) ) ) 138 | 139 | @property 140 | def width(self): 141 | return MagickGetImageWidth( self._wand ) 142 | 143 | @property 144 | def height(self): 145 | return MagickGetImageHeight( self._wand ) 146 | 147 | def scale( self, size ): 148 | ''' Scales the size of image to the given dimensions. 149 | size - A tuple containing the size of the scaled image.''' 150 | MagickScaleImage( self._wand, size[0], size[1] ) 151 | 152 | def get_size( self ): 153 | return ( self.width, self.height ) 154 | size = property( get_size, scale, None, 'A tuple containing the size of the image. Setting the size is the same as calling scale().' ) 155 | 156 | @property 157 | def meta(self): 158 | return {'format': self.format, 'width': int(self.width), 'height': int(self.height), 'quality': int(self.quality)} 159 | 160 | def get_blob(self): 161 | size = ctypes.c_size_t() 162 | b = MagickGetImageBlob( self._wand, ctypes.byref(size) ) 163 | if b and size.value: 164 | blob = ctypes.string_at(b, size.value) 165 | MagickRelinquishMemory(b) 166 | return blob 167 | self.error() 168 | 169 | def save( self, file = None ): 170 | ''' Saves the image to a file. If no file is specified, the file is 171 | saved with the original filename.''' 172 | if hasattr( file, 'write' ): 173 | return file.write( self.get_blob() ) 174 | else: 175 | r = MagickWriteImage( self._wand, file ) 176 | 177 | if not r: 178 | self.error() 179 | return r 180 | 181 | def thumbnail( self, columns, rows = None, fit = True, max_width = 0, max_height = 0 ): 182 | if rows is None: rows = columns 183 | print "thumbnail columns: {}, rows: {}, max_width: {}, max_height: {}".format(columns, rows, max_width, max_height) 184 | 185 | org_width, org_height = self.size 186 | 187 | if org_width <= columns and org_height <= rows: 188 | if MagickStripImage(self._wand): 189 | return True 190 | return False 191 | 192 | if fit: 193 | rel = float( org_width ) / float( org_height ) 194 | if max_width > 0: 195 | columns = max_width 196 | rows = int( columns / rel ) 197 | elif max_height > 0: 198 | rows = max_height 199 | columns = int( rows * rel ) 200 | else: 201 | bounds = float( columns ) / float( rows ) 202 | if rel >= bounds: rows = int( columns / rel ) 203 | else: columns = int( rows * rel ) 204 | print "fit columns: {}, rows: {}".format(columns, rows) 205 | if not MagickThumbnailImage( self._wand, columns, rows ): 206 | print('error: MagickThumbnailImage') 207 | 208 | self.error() 209 | 210 | return False 211 | return True 212 | 213 | 214 | def cropThumbnail( self, dst_width, dst_height = None ): 215 | if dst_height is None: dst_height = dst_width 216 | 217 | org_width, org_height = self.size 218 | 219 | if org_width <= dst_width and org_height <= dst_height: 220 | if MagickStripImage(self._wand): 221 | return True 222 | return False 223 | 224 | ratio_x = float( dst_width ) / float( org_width ); 225 | ratio_y = float( dst_height ) / float( org_height ); 226 | 227 | if ratio_x > ratio_y: 228 | new_width = int(dst_width) 229 | new_height = int(ratio_x * float( org_height )) 230 | else: 231 | new_height = int(dst_height) 232 | new_width = int(ratio_y * float( org_width )) 233 | 234 | if not MagickThumbnailImage(self._wand, new_width, new_height): 235 | print('error: MagickThumbnailImage') 236 | return False 237 | 238 | if new_width == dst_width and new_height == dst_height: 239 | return True 240 | 241 | crop_x = int((new_width - dst_width) / 2); 242 | crop_y = int((new_height - dst_height) / 2); 243 | 244 | print "crop_x: {0}, crop_y: {1}".format(crop_x, crop_y) 245 | 246 | if not MagickCropImage(self._wand, dst_width, dst_height, crop_x, crop_y): 247 | print('error: MagickCropImage') 248 | return False 249 | 250 | r = MagickSetImagePage(self._wand, dst_width, dst_height, 0, 0); 251 | 252 | if not r: 253 | self.error() 254 | 255 | return True 256 | 257 | def watermark(self, image, transparency=0.0, left=0, top=0, position=None, copyright=None): 258 | """ 259 | watermark methods: 260 | 1. convert bgnd overlay -compose modulate \ 261 | -define compose:args={brigthness}[,{saturation}] \ 262 | -composite result 263 | 2. convert bgnd overlay -compose dissolve \ 264 | -define compose:args={src_percent},{dst_percent} \ 265 | -composite result 266 | """ 267 | watermark_image = image.clone() 268 | s_width, s_height = self.size 269 | w_width, w_height = watermark_image.size 270 | 271 | if s_width < w_width or s_height < w_height: 272 | print 'source image is too small, must large than {} x {}'.format(w_width, w_height) 273 | return False 274 | 275 | if position == 'bottom-right': 276 | left = s_width - w_width - 10 277 | top = s_height - w_height - 10 278 | elif position == 'top-left': 279 | left = top = 10 280 | elif position == 'top-right': 281 | left = s_width - w_width - 10 282 | top = 10 283 | elif position == 'bottom-left': 284 | left = 10 285 | top = s_height - w_height - 10 286 | elif position == 'center': 287 | left = (s_width - w_width) / 2 288 | top = (s_height - w_height) / 2 289 | elif position == 'golden': 290 | #left = s_width * 0.382 - w_width / 2 291 | left = (s_width - w_width) / 2 292 | top = s_height * 0.618 - w_height / 2 293 | 294 | MagickSetImageArtifact(watermark_image.wand,"compose:args", "15%") 295 | #MagickSetImageArtifact(watermark_image.wand,"compose:args", "5") 296 | #MagickSetImageGravity(watermark_image.wand, SouthGravity) 297 | #op = DissolveCompositeOp 298 | #op = ModulateCompositeOp 299 | op = BlendCompositeOp 300 | r = MagickCompositeImage(self.wand, watermark_image.wand, op, int(left), int(top)) 301 | del watermark_image 302 | 303 | if not r: 304 | self.error() 305 | 306 | if copyright and isinstance(copyright, SimpImage): 307 | ci = copyright.clone() 308 | MagickSetImageArtifact(ci.wand,"compose:args", "40%") 309 | MagickCompositeImage(self.wand, ci.wand, op, int(s_width * 0.382 - w_width / 2), int(s_height - w_height - s_height*.1)) 310 | del ci 311 | 312 | return r 313 | 314 | 315 | def error(self, stacklevel=1): 316 | severity = ctypes.c_int() 317 | desc = MagickGetException(self.wand, ctypes.byref(severity)) 318 | print severity 319 | print desc 320 | MagickClearException(self.wand) 321 | # TODO: process exception or warning 322 | #if isinstance(e, Warning): 323 | # warnings.warn(e, stacklevel=stacklevel + 1) 324 | #elif isinstance(e, Exception): 325 | # raise e 326 | -------------------------------------------------------------------------------- /imsto/migrate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | tool.py 5 | 6 | Created by liut on 2013-06-18. 7 | Copyright (c) 2010-2013 liut. All rights reserved. 8 | """ 9 | 10 | import sys 11 | import os 12 | from numbers import Integral 13 | from store import load_imsto, Config, encode_upload 14 | # from _config import log 15 | 16 | 17 | def test_section(section): 18 | config = Config() 19 | return config.has_section(section) 20 | 21 | def migrate(from_section, to_section, skip=0, limit=50, end_count=0, id=None): 22 | """merge and sync data between 2 storage engines""" 23 | if from_section == to_section: 24 | return False 25 | 26 | if not test_section(from_section) or not test_section(to_section): 27 | return False 28 | 29 | imsto1 = load_imsto(from_section) 30 | imsto2 = load_imsto(to_section) 31 | 32 | if id is not None: 33 | item = imsto1.get_meta(id) 34 | if item: 35 | print "start trans {},{},{}".format(item.id, item.filename, item.size) 36 | r = _store_item(imsto2, item) 37 | print 'trans ok: %s, %s' % (r, item.name) 38 | else: 39 | print 'item id %s not found' % id 40 | return 41 | 42 | total = imsto1.count() 43 | print 'total: {}'.format(total) 44 | 45 | p_count = 0 46 | offset = skip if isinstance(skip, Integral) and skip > 0 else 0 47 | while offset < total: 48 | print 'migrating page {}/{}'.format(offset, total) 49 | i = offset 50 | for item in imsto1.browse(limit, offset, only_items=True): 51 | print "start trans {:4d} {},{},{}".format(i, item.id, item.filename, item.size) 52 | r = _store_item(imsto2, item) 53 | print 'trans ok: %s, %s' % (r, item.name) 54 | i += 1 55 | p_count += 1 56 | if end_count > 0 and p_count >= end_count: 57 | print 'end count done' 58 | return 59 | offset += limit 60 | 61 | print 'all done!' 62 | 63 | def _store_item(imsto2, item): 64 | # print 'item size: %s' % item.size 65 | return imsto2.store(item.file, ctype=item.mime, name=item.name, created=item.created, id=item.id) 66 | 67 | if __name__ == '__main__': 68 | import argparse 69 | parser = argparse.ArgumentParser(usage='%(prog)s command') 70 | parser.add_argument('-F','--src',metavar='section',type=str) 71 | parser.add_argument('-T','--dst',metavar='section',type=str) 72 | parser.add_argument('--skip', type=int, default=0) 73 | parser.add_argument('--count', type=int, default=0) 74 | parser.add_argument('--id', type=str, default=None) 75 | args, remaining = parser.parse_known_args() 76 | 77 | #migrate('imsto', 'weed') 78 | if args.src and args.dst: 79 | migrate(args.src, args.dst, skip=args.skip, end_count=args.count, id=args.id) 80 | else: 81 | parser.print_help() 82 | -------------------------------------------------------------------------------- /imsto/store.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | store.py 4 | 5 | imsto: core module 6 | 7 | Created by liut on 2010-12-16. 8 | Copyright (c) 2010-2013 liut. All rights reserved. 9 | """ 10 | 11 | import os,re,datetime 12 | from urlparse import urljoin 13 | from hashlib import md5 14 | from numbers import Integral 15 | from pymongo import ASCENDING, DESCENDING, MongoClient, MongoReplicaSetClient, ReadPreference 16 | from _config import Config 17 | from _base import base_convert 18 | from _util import * 19 | 20 | __all__ = [ 21 | 'load_imsto', 22 | 'EngineError', 'UrlError', 'HttpFound', 'DuplicateError', 23 | ] 24 | 25 | def load_imsto(section='imsto'): 26 | config = Config() 27 | engine = config.get('engine', section) 28 | print 'loading {} engine: {}'.format(section, engine) 29 | if engine == 'mongodb': 30 | return StoreEngineGridFs(section) 31 | if engine == 's3': 32 | return StoreEngineS3(section) 33 | if engine == 'weedfs': 34 | return StoreEngineWeedFs(section) 35 | raise ValueError('bad engine_code') 36 | 37 | class StoreBase: 38 | engine = None 39 | _db = None 40 | _fs = None 41 | _coll = None 42 | 43 | def __init__(self, section='imsto'): 44 | """engine: mongodb(default), s3""" 45 | self.section = section 46 | self._config = Config() 47 | 48 | self.engine = self.get_config('engine') 49 | self.fs_prefix = self.get_config('fs_prefix') 50 | print 'init section: {self.section}, engine: {self.engine}, fs_prefix: {self.fs_prefix}'.format(self=self) 51 | 52 | def get_config(self, key): 53 | return self._config.get(key, self.section) 54 | 55 | def browse(self, limit=20, start=0, sort=None, only_items = False): 56 | """retrieve files from mongodb for gallery""" 57 | #return fs().list() 58 | if sort is None or not isinstance(sort, list): 59 | sort = [('uploadDate',DESCENDING)] 60 | 61 | cursor = self.collection.find(limit=limit,skip=start,sort=sort) 62 | items = [StoreItem(self, item) for item in cursor] 63 | if only_items: 64 | return items 65 | url_prefix = urljoin(self.get_config('url_prefix'), self.get_config('thumb_path')) 66 | return {'items':items,'total':cursor.count(),'url_prefix': url_prefix + '/'} 67 | 68 | def count(self): 69 | return self.collection.count(); 70 | 71 | # def __iter__(self): 72 | # self.__cursor = self.collection.find(limit=0,skip=0,sort=[('uploadDate',DESCENDING)]) 73 | # return self 74 | # def next(self): 75 | # if self.__cursor: 76 | # return StoreItem(self, self.__cursor.next()) 77 | # raise StopIteration 78 | 79 | def store(self, file=None, content=None, ctype=None, **kwd): 80 | """save a file-like item""" 81 | if content is None and not hasattr(file, 'read'): 82 | raise TypeError('invalid file-like object') 83 | 84 | data = content if content is not None else file.read() 85 | size = len(data) 86 | ext = guessImageType(data[:32]) 87 | if ext is None: 88 | raise ValueError('invalid image file') 89 | 90 | hashes = [md5(data).hexdigest()] 91 | _exists_id = self.exists(hashed=hashes[0]) 92 | if _exists_id: 93 | id = _exists_id 94 | filename = _make_filename(id, ext) 95 | print ('id {} or hash {} exists!!'.format(id, hashes[0])) 96 | #raise DuplicateError('already exists') 97 | return [True, id, filename] 98 | ids = [_make_id(hashes[0])] 99 | if 'id' in kwd and kwd['id'] and kwd['id'] not in ids: 100 | ids += [kwd['id']] 101 | 102 | from image import SimpImage, MIN_QUALITY 103 | 104 | max_file_size = int(self.get_config('max_file_size')) 105 | max_jpeg_quality = int(self.get_config('max_jpeg_quality')) 106 | max_width = int(self.get_config('max_width')) 107 | max_height = int(self.get_config('max_height')) 108 | 109 | if size > max_file_size: max_jpeg_quality -= 1 110 | if max_jpeg_quality < MIN_QUALITY: max_jpeg_quality = MIN_QUALITY 111 | 112 | im = SimpImage(blob=data) 113 | meta = im.meta 114 | if meta['width'] > max_width or meta['height'] > max_height: 115 | if self.get_config('auto_scale') and im.thumbnail(max_width, max_height): 116 | if im.format == 'JPEG' and im.quality > max_jpeg_quality: 117 | im.quality = max_jpeg_quality 118 | data = im.get_blob() 119 | size = len(data) 120 | print im.meta 121 | print 'new scaled size {}'.format(size) 122 | hashes += [md5(data).hexdigest()] 123 | else: 124 | raise ValueError('file: {} dimension {}x{} is too big, max is {}x{}'.format(kwd['name'] if 'name' in kwd else '', meta['width'], meta['height'], max_width, max_height)) 125 | 126 | if im.format == 'JPEG': 127 | if im.quality > max_jpeg_quality: 128 | print 'quality {} is too high, hash {}'.format(im.quality, hashes[0]) 129 | from tempfile import NamedTemporaryFile 130 | _tmp = NamedTemporaryFile('w+b',dir=self.get_config('temp_root'),delete=False) 131 | _tmp.file.close() 132 | save_file(_tmp.name, blob=data) 133 | if jpegoptim(_tmp.name): 134 | fp = open(_tmp.name) 135 | data = fp.read() 136 | size = len(data) 137 | 138 | # print 'new optimized size {}'.format(size) 139 | fp.close() 140 | _tmp.unlink(_tmp.name) 141 | del im 142 | im = SimpImage(blob=data) 143 | meta = im.meta 144 | hashes += [md5(data).hexdigest()] 145 | else: 146 | raise EnvironmentError('jpeg qualty is too high, or need jpegoptim') 147 | elif im.format == 'PNG' and self.get_config('force_jpeg'): 148 | im.format = 'JPEG' 149 | im.quality = max_jpeg_quality 150 | data = im.get_blob() 151 | size = len(data) 152 | hashes += [md5(data).hexdigest()] 153 | ext = 'jpg' 154 | meta = im.meta 155 | del im 156 | 157 | if (size > max_file_size): 158 | raise ValueError('file: {} size {} is too big, max is {}'.format(kwd['name'] if 'name' in kwd else '', size, max_file_size)) 159 | 160 | hashed = hashes[len(hashes)-1] #md5(data).hexdigest() 161 | # print ('md5 hash: {}'.format(hashed)) 162 | 163 | # TODO: add for support (md5 + size) id 164 | id = _make_id(hashed) 165 | 166 | # print ('new filename: %r' % filename) 167 | 168 | # TODO: fix for support s3 front browse 169 | _exists_id = self.exists(id) or self.exists(hashed=hashed) 170 | if _exists_id: 171 | id = _exists_id 172 | filename = _make_filename(id, ext) 173 | print ('id {} or hash {} exists!!'.format(id, hashed)) 174 | #raise DuplicateError('already exists') 175 | return [True, id, filename] 176 | filename = _make_filename(id, ext) 177 | # print ('id: {}'.format(id)) 178 | 179 | # if ctype is None or ctype == '': 180 | from _util import guess_mimetype 181 | ctype = guess_mimetype(filename) 182 | 183 | # save to mongodb 184 | spec = {'_id': id,'filename': filename, 'hash': hashes, 'mime': ctype, 'size': size, 'meta': meta, 'ids': ids} 185 | 186 | if 'name' in kwd and isinstance(kwd['name'], (str, unicode)): 187 | spec['name'] = kwd['name'] 188 | 189 | for k in ['created', 'app_id']: 190 | if k in kwd and kwd[k]: 191 | spec[k] = kwd[k] 192 | 193 | if self._store_exists(id, filename=filename): 194 | self._save_meta(id, spec) 195 | return [True, id, filename] 196 | 197 | rr = self._put(data, **spec) 198 | if rr: 199 | return [True, rr, filename] 200 | 201 | def get_meta(self, id=None, filename=None, ids=None): 202 | spec = None 203 | if id: 204 | spec = id 205 | elif filename: 206 | spec = {'filename': filename} 207 | elif ids and isinstance(ids, type([])): 208 | spec = {'ids': {'$in': ids}} 209 | 210 | if spec: 211 | print 'spec %s' % spec 212 | item = self.collection.find_one(spec) 213 | if item: 214 | return StoreItem(self, item) 215 | 216 | def _save_meta(self, id, spec): 217 | '''mongo special meta data''' 218 | #if not hasattr(spec, '_id'): 219 | # spec['_id'] = id 220 | if 'created' not in spec: 221 | spec['created'] = datetime.datetime.utcnow() 222 | 223 | if 'filename' not in spec: 224 | print spec 225 | raise ValueError('need filename') 226 | 227 | return self.collection.update({'_id': id}, spec, upsert=True) 228 | 229 | def delete(self, id): 230 | raise NotImplemented() 231 | 232 | def _get(self, id): 233 | raise NotImplemented() 234 | 235 | def _put(self, data, **spec): 236 | raise NotImplemented() 237 | 238 | def _store_exists(self, id=None, *args, **kwargs): 239 | raise NotImplemented() 240 | 241 | def exists(self, id=None, hashed=None, filename=None, *args, **kwargs): 242 | """check special hash value TODO: more args""" 243 | #print args 244 | #print kwargs 245 | if id and self.collection.find_one({"_id": id}): 246 | return id 247 | if hashed: 248 | doc = self.collection.find_one({'md5': hashed}) 249 | if doc: 250 | return doc['_id'] 251 | doc = self.collection.find_one({'hash': {'$in': [hashed]}}) 252 | if doc: 253 | return doc['_id'] 254 | 255 | if filename: 256 | doc = self.collection.find_one(filename=filename) 257 | if doc: 258 | return doc['_id'] 259 | 260 | if self._store_exists(id, hashed=hashed, filename=filename, *args, **kwargs): 261 | return id 262 | 263 | @property 264 | def db(self): 265 | if self._db is None: 266 | self._db = get_mongo_db(self.get_config('servers'), self.get_config('db_name'), self.get_config('replica_set')) 267 | return self._db 268 | 269 | @property 270 | def collection(self): 271 | if self._coll is None: 272 | cn = '{0}.files'.format(self.fs_prefix) 273 | self._coll = self.db[cn] 274 | return self._coll 275 | 276 | def close(self): 277 | """ close db connection""" 278 | if self.db is not None: 279 | self.db.connection.disconnect() 280 | 281 | def load(self, path): 282 | """ load from url path """ 283 | #print 'path: %s (%s)' % (path, type(path)) 284 | image_url_regex = r'(?P[scwh]\d{2,4}(?Px\d{2,4})?|orig)(?P[a-z])?/(?P[a-z0-9]{2})/(?P[a-z0-9]{2})/(?P[a-z0-9]{19,36})\.(?Pgif|jpg|jpeg|png)$' 285 | match = re.search(image_url_regex, path) 286 | #print(image_url_regex, path, match) 287 | if match is None: 288 | raise UrlError('invalid path') 289 | 290 | ids = match.groupdict() 291 | #print(ids) 292 | 293 | id = '{t1}{t2}{t3}'.format(**ids) 294 | 295 | THUMB_ROOT = self.get_config('thumb_root').rstrip('/') 296 | SUPPORTED_SIZE = self.get_config('support_size').split(',') 297 | 298 | org_path = '{t1}/{t2}/{t3}.{ext}'.format(**ids) 299 | org_file = '{0}/orig/{1}'.format(THUMB_ROOT, org_path) 300 | 301 | if not os.path.exists(org_file): 302 | 303 | # check old id for redirect 304 | doc = self.get_meta(ids=[id]) 305 | if doc and doc['id'] != id and 'filename' in doc: 306 | print 'found %s' % doc['filename'] 307 | thumb_path = self.get_config('thumb_path') 308 | new_path = '{}/{}/{}'.format(thumb_path, ids['size'], doc['filename']) 309 | raise HttpFound('found', path=new_path) 310 | 311 | print('fetching file: {}'.format(org_path)) 312 | file = self.fetch(id, path=org_path) 313 | if file is None: 314 | print('fetch failed') 315 | raise UrlError('id {} not found'.format(id)) 316 | save_file(org_file, file) 317 | 318 | if not os.path.exists(org_file): 319 | raise UrlError('file not found') 320 | 321 | # start thumbnail image 322 | 323 | if ids['size'] == 'orig': 324 | dst_path = 'orig/{}'.format(org_path) 325 | dst_file = org_file 326 | else: 327 | dst_path = '{0}/{1}'.format(ids['size'], org_path) 328 | dst_file = '{0}/{1}'.format(THUMB_ROOT, dst_path) 329 | 330 | mode = ids['size'][0] 331 | dimension = ids['size'][1:] 332 | if dimension not in SUPPORTED_SIZE: 333 | #print('unsupported size: {} {}'.format(mode, dimension)) 334 | raise UrlError('unsupported size') 335 | if ids['x'] is None: 336 | size = int(dimension) 337 | width, height = size, size 338 | else: 339 | width, height = map(int, dimension.split('x')) 340 | 341 | if not os.path.exists(dst_file): 342 | print('start thumbnail image {} {} => {}x{}'.format(mode, dimension, width, height)) 343 | thumb_image(org_file, width, height, dst_file, mode) 344 | 345 | if ids['mop'] == 'w' and width < 100: 346 | raise UrlError('bad size') 347 | 348 | if ids['mop'] is not None: 349 | if ids['mop'] == 'w': # watermark modifier 350 | org_file = '{}/{}/{}'.format(THUMB_ROOT, ids['size'], org_path) 351 | dst_file = '{}/{}{}/{}'.format(THUMB_ROOT, ids['size'], ids['mop'], org_path) 352 | 353 | if watermark_image(org_file, dst_file): 354 | dst_path = '{}{}/{}'.format(ids['size'], ids['mop'], org_path) 355 | 356 | else: 357 | raise UrlError('bad modifier') 358 | 359 | #print('dst_path: {}'.format(dst_path)) 360 | #print('dst_file: {}'.format(dst_file)) 361 | 362 | return (dst_file, dst_path) 363 | 364 | def fetch(self, id, path): 365 | key = path if self.engine == 's3' else id 366 | 367 | return self._get(key) 368 | # try: 369 | # return self._get(key) 370 | # except Exception, e: 371 | # print('prepare: {} not found'.format(key)) 372 | # print e 373 | # raise e 374 | 375 | 376 | 377 | def url(self, path, size='orig'): 378 | url_prefix = self.get_config('url_prefix') 379 | thumb_path = self.get_config('thumb_path') 380 | return '{}/{}/{}/{}'.format(url_prefix.rstrip('/'), thumb_path.strip('/'), size, path) 381 | 382 | class StoreItem(dict): 383 | """docstring for Item""" 384 | imsto = None 385 | id = None 386 | def __init__(self, imsto, meta): 387 | self.imsto = imsto 388 | if isinstance(meta, dict): 389 | item = self._fix_meta(meta) 390 | for key, value in item.iteritems(): 391 | self[key.lower()] = value 392 | if key == 'id': 393 | self.id = value 394 | # if not self.has_key('name') or self['name'] is None: 395 | # self.name = self.id + guess_ext(self['mime']) 396 | 397 | def __getattr__(self, name): 398 | if name == 'dict': 399 | return self 400 | else: 401 | return self.get(name, None) 402 | 403 | def _fix_meta(self, item): 404 | '''convert mongo item to simple''' 405 | item['id'] = item.pop('_id') 406 | if item.has_key('length'): 407 | item['size'] = item.pop('length') 408 | elif item.has_key('content_length'): 409 | item['size'] = item.pop('content_length') 410 | if item.has_key('uploadDate'): 411 | item['created'] = item.pop('uploadDate') 412 | if item.has_key('contentType'): 413 | item['mime'] = item.pop('contentType') 414 | if item.has_key('content_type'): 415 | item['mime'] = item.pop('content_type') 416 | if not item.has_key('filename') and item.has_key('path'): 417 | item['filename'] = item.pop('path') 418 | item.pop('chunkSize', None) 419 | item.pop('app_id', None) 420 | if item.has_key('md5') and not item.has_key('hash'): 421 | item['hash'] = [item['md5']] 422 | # item.pop('md5', None) 423 | # print item 424 | return item 425 | 426 | @property 427 | def file(self): 428 | return self.imsto._get(self) 429 | 430 | 431 | class EngineError(Exception): 432 | """ Invalid Engine """ 433 | pass 434 | 435 | class UrlError(Exception): 436 | """ Invalid Url or path """ 437 | pass 438 | 439 | class HttpFound(Exception): 440 | """ TODO: path has been changed """ 441 | 442 | def __init__(self, message, path, **kwds): 443 | self.args = message, kwds.copy() 444 | self.msg, self.extra = self.args 445 | self.path = path 446 | 447 | 448 | class DuplicateError(Exception): 449 | """ Entry Duplicated """ 450 | pass 451 | 452 | def get_mongo_db(host_or_uri, db_name, replica_set = None): 453 | if replica_set: 454 | c = MongoReplicaSetClient(host_or_uri, replicaSet=replica_set,read_preference=ReadPreference.NEAREST) 455 | else: 456 | c = MongoClient(host_or_uri,read_preference=ReadPreference.NEAREST) 457 | return c[db_name] 458 | 459 | def _make_id(hashed, size=None): 460 | """make mongo item id by file hash value""" 461 | if size is None or size < 1: 462 | return base_convert(hashed, 16, 36) 463 | if not isinstance(size, Integral): 464 | raise TypeError('expected a int, not ' + repr(size)) 465 | return base_convert('{}{:02x}'.format(hashed, size % 255), 16, 36) 466 | 467 | def _make_filename(id, ext): 468 | match = re.match('([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{16,36})',id) 469 | if match: 470 | return '{0[0]}/{0[1]}/{0[2]}.{1}'.format(match.groups(), ext) 471 | raise ValueError('invalid id %s' % id) 472 | # return id + '.' + ext 473 | 474 | 475 | class StoreEngineGridFs(StoreBase): 476 | """docstring for StoreEngineGridFs""" 477 | _db = None 478 | _fs = None 479 | def __init__(self, section): 480 | StoreBase.__init__(self, section) 481 | 482 | def _get(self, id): 483 | if isinstance(id, StoreItem): 484 | id = id.id 485 | if id and self.exists(id): 486 | return self.fs.get(id) 487 | 488 | def delete(self, id): 489 | self.fs.delete(id) 490 | if self.fs.exists(id): 491 | return False 492 | return True 493 | 494 | def _put(self, data, **spec): 495 | return self.fs.put(data, **spec) 496 | 497 | def _store_exists(self, id=None, *args, **kwargs): 498 | #print id 499 | return self.fs.exists(id) 500 | 501 | @property 502 | def fs(self): 503 | if not self._fs: 504 | import gridfs 505 | self._fs = gridfs.GridFS(self.db,self.fs_prefix) 506 | 507 | return self._fs 508 | 509 | class StoreEngineS3(StoreBase): 510 | """docstring for StoreEngineS3""" 511 | _bucket = None 512 | def __init__(self, section): 513 | StoreBase.__init__(self, section) 514 | 515 | def _get(self, id): 516 | if isinstance(id, str): 517 | if id.rfind('.') > 0: 518 | key = id 519 | else: 520 | item = self.get_meta(id) 521 | if item: 522 | key = item.filename 523 | # print 'found %s' % item 524 | elif isinstance(id, StoreItem): 525 | key = id.filename 526 | else: 527 | raise ValueError('invalid id or key') 528 | return self.bucket.get(key) 529 | 530 | def delete(self, id): 531 | key = None 532 | if isinstance(id, str): 533 | item = self.get_meta(id) 534 | if item: 535 | print 'found item id: %s' % id 536 | key = item.filename 537 | elif isinstance(id, StoreItem): 538 | key = id.filename 539 | id = id.id 540 | else: 541 | raise ValueError('invalid id or key') 542 | if key is not None and id and self.bucket.delete(key): 543 | print 'delete filename: %s ok' % key 544 | r = self.collection.remove(id,safe=True) 545 | return 'ok' in r and r['err'] is None 546 | return False 547 | 548 | def _put(self, data, **spec): 549 | 550 | metadata = {} 551 | for k in spec['meta']: 552 | metadata[k] = str(spec['meta'][k]) 553 | 554 | if 'name' in spec and isinstance(spec['name'], (str, unicode)): 555 | from urllib import quote_plus 556 | metadata['name'] = quote_plus(spec['name'].encode('utf-8') if isinstance(spec['name'], unicode) else spec['name']) 557 | # print metadata['name'] 558 | 559 | headers = {'Content-Length': spec['size']} 560 | try: 561 | filename = spec['filename'] 562 | self.bucket.put(filename, data=data, mimetype=spec['mime'], metadata=metadata, headers=headers) 563 | print "save ok %s to s3" % filename 564 | self._save_meta(spec['_id'], spec) 565 | print "save ok meta %s" % spec['_id'] 566 | return spec['_id'] 567 | except Exception, e: 568 | raise e 569 | 570 | 571 | def _store_exists(self, id=None, *args, **kwargs): 572 | if 'filename' in kwargs and kwargs['filename'] is not None: 573 | return kwargs["filename"] in self.bucket 574 | return False 575 | 576 | @property 577 | def bucket(self): 578 | if self._bucket is None: 579 | from simples3 import S3Bucket, KeyNotFound 580 | bucket_name = self.get_config('bucket_name') 581 | if bucket_name is None: 582 | raise ValueError("no bucket_name in section '%s'" % self.section) 583 | access_key = self.get_config('s3_access_key') 584 | secret_key = self.get_config('s3_secret_key') 585 | self._bucket = S3Bucket(bucket_name, access_key=access_key, secret_key=secret_key) 586 | return self._bucket 587 | 588 | WEED_HOST = 'weed_vol_host' 589 | WEED_FID = 'weed_fid' 590 | 591 | class StoreEngineWeedFs(StoreBase): 592 | """docstring for StoreEngineWeedFs""" 593 | def __init__(self, section): 594 | StoreBase.__init__(self, section) 595 | from weedfs import WeedClient 596 | self.client = WeedClient() 597 | 598 | def _get(self, id): 599 | print '_get {}'.format(id) 600 | item = id if isinstance(id, StoreItem) else self.get_meta(id) 601 | if not item.has_key(WEED_HOST) or not item.has_key(WEED_HOST): 602 | raise ValueError('the entry has no special value ' + WEED_HOST + ' and ' + WEED_FID) 603 | volume_host, fid = item[WEED_HOST], item[WEED_FID] 604 | ctype, size, content = self.client.retrieve(volume_host, fid) 605 | print 'weed retrieved: %s %s' % (ctype, size) 606 | if content: 607 | from StringIO import StringIO 608 | return StringIO(content) 609 | raise ValueError('weed client.retrieve error: invalid response') 610 | 611 | def delete(self, id): 612 | raise NotImplemented() 613 | 614 | def _put(self, data, **spec): 615 | volume_host, fid = self.client.assign() 616 | ret = self.client.store(volume_host, fid, content=data, name=spec['filename'], content_type=spec['mime']) 617 | if isinstance(ret, int) and ret > 0: 618 | print 'saved {}/{} size {} bytes'.format(volume_host, fid, ret) 619 | spec[WEED_HOST] = volume_host 620 | spec[WEED_FID] = fid 621 | self._save_meta(spec['_id'], spec) 622 | return spec['_id'] 623 | print 'store error: %s' % ret 624 | 625 | 626 | def _store_exists(self, id=None, *args, **kwargs): 627 | if hasattr(kwargs, WEED_HOST) and hasattr(kwargs, WEED_FID): 628 | ctype, size = self.client.retrieve(kwargs[WEED_HOST],kwargs[WEED_FID], head=True) 629 | print 'exists %s %s' % (ctype, size) 630 | return True 631 | return False 632 | 633 | 634 | 635 | -------------------------------------------------------------------------------- /imsto/tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """ 4 | tool.py 5 | 6 | Created by liut on 2010-12-24. 7 | Copyright (c) 2010-2013 liut. All rights reserved. 8 | """ 9 | 10 | import sys 11 | import os 12 | from store import load_imsto, Config 13 | 14 | 15 | section = 'imsto' 16 | 17 | 18 | def list_dir(limit=5,start=0,prefix=''): 19 | imsto = load_imsto(section) 20 | gallery = imsto.browse(limit, start) 21 | if gallery['total'] == 0: 22 | print 'total 0, empty' 23 | return 24 | 25 | print 'total {}'.format(gallery['total']) 26 | for item in gallery['items']: 27 | #print hasattr(item.file, 'read') 28 | #print(item) 29 | print("{0[filename]}\t{0[size]:8,d}".format(item)) 30 | 31 | def store_file(filename): 32 | if os.access(filename, os.R_OK): 33 | imsto = load_imsto(section) 34 | from _util import guess_mimetype 35 | ctype = guess_mimetype(filename) 36 | with open(filename) as fp: 37 | ret = imsto.store(fp, ctype, name=os.path.basename(filename)) 38 | print ret 39 | else: 40 | print 'image {} not found or access deny'.format(filename) 41 | 42 | 43 | if __name__ == "__main__": 44 | import argparse 45 | config = Config() 46 | parser = argparse.ArgumentParser(usage='%(prog)s [options]') 47 | parser.add_argument('-s', '--section', metavar='section', default='imsto', choices=config.sections(), type=str, help='Special config section') 48 | parser.add_argument('-i', '--add', metavar='filename', type=str, help='Import file to storeage') 49 | parser.add_argument('-q', '--query', metavar='[exist|meta]', type=str, choices=['exist', 'meta'], help='query a file') 50 | parser.add_argument('-f', '--fetch', metavar='path', type=str, help='fetch a file') 51 | parser.add_argument('--id', metavar='id', type=str, help='Special file id') 52 | parser.add_argument('--path', metavar='path', type=str, help='Special file path') 53 | parser.add_argument('-v', '--verbose', action='store_true') 54 | parser.add_argument('-l', '--list', action='store_true', help='List files') 55 | #default=argparse.SUPPRESS 56 | parser.add_argument('--limit', type=int, default=5) 57 | parser.add_argument('--start', type=int, default=0) 58 | parser.add_argument('--prefix', type=str, default='') 59 | args, remaining = parser.parse_known_args() 60 | #print args 61 | 62 | section = args.section 63 | print section 64 | if args.list: 65 | list_dir(args.limit, args.start, prefix=args.prefix) 66 | elif args.fetch: 67 | imsto = load_imsto(section) 68 | _file, _path = imsto.load('orig/{}'.format(args.fetch)) 69 | print _path 70 | elif args.query: 71 | imsto = load_imsto(section) 72 | method = imsto.get_meta if args.query == 'meta' else imsto.exists 73 | # print method 74 | print method(args.id or None,filename=args.path or None) 75 | elif args.add: 76 | store_file(filename=args.add) 77 | else: 78 | parser.print_help() 79 | #sys.exit(main()) 80 | 81 | -------------------------------------------------------------------------------- /imsto/weedfs.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | """ 3 | weedfs.py 4 | 5 | Created by liut on 2013-06-09. 6 | Copyright (c) 2010-2013 liut. All rights reserved. 7 | """ 8 | 9 | 10 | import os 11 | import urllib 12 | import httplib2 13 | import json 14 | from _util import * 15 | 16 | class WeedClient(object): 17 | """Client for weed-fs 18 | Learn from: https://github.com/micjohnson/weed-php 19 | """ 20 | def __init__(self, master = 'localhost:9333', replication = None): 21 | self.master = master 22 | self.default_replication = replication 23 | 24 | def _request(self, url, method = 'GET', body = None, headers = None): 25 | print '_request: %s' % url 26 | h = httplib2.Http() 27 | if method == 'POST': 28 | resp, content, = h.request(url, method, body=body, headers=headers) 29 | else: 30 | resp, content = h.request(url, method) 31 | 32 | #print resp 33 | #print content 34 | try: 35 | if resp.status >= 400: 36 | print 'request error: {} {}'.format(resp.status, resp.reason) 37 | print 'response: {}'.format(content) 38 | return resp.status, resp.reason, json.loads(content) if content.startswith('{') else content 39 | if resp['content-type'] == 'application/javascript' or resp.status == 201 and resp['content-type'] == 'text/plain; charset=utf-8': 40 | return resp.status, resp.reason, json.loads(content) 41 | # get content 42 | return resp['content-type'], int(resp['content-length']), content 43 | except Exception, e: 44 | print e 45 | print resp 46 | return resp.status, e, None 47 | 48 | def assign(self, count = 1, replication = None): 49 | url = 'http://{}/dir/assign?count={}'.format(self.master,int(count)) 50 | if replication is None: 51 | replication = self.default_replication 52 | if replication: 53 | url = '{}&replication={}'.format(url, replication) 54 | #{"count":1,"fid":"2,753b19e78fe6","publicUrl":"127.0.0.1:9334","url":"localhost:9334"} 55 | first, second, result = self._request(url) 56 | #volume_host, fid = 57 | if first >= 200 and first < 300 and isinstance(result, dict): 58 | return result['publicUrl'], result['fid'] 59 | print 'error assign {}: {}, content: {}'.format(first, second, result) 60 | 61 | def status(self): 62 | url = 'http://{}/dir/status'.format(self.master) 63 | 64 | return self._request(url) 65 | 66 | def retrieve(self, volume_host, fid, head=False): 67 | url = 'http://{}/{}'.format(volume_host, fid) 68 | #f = urllib.urlopen(url) 69 | first, second, content = self._request(url, 'HEAD' if head else 'GET') 70 | #print 'retrieve %s %s' % (first, second) 71 | if isinstance(first, str): 72 | return first, second, content 73 | print 'error retrieve: {}: {}, content: {}'.format(first, second, len(content)) 74 | return first, second, None 75 | 76 | def delete(self, volume_host, fid): 77 | url = 'http://{}/{}'.format(volume_host, fid) 78 | return self._request(url, 'DELETE') 79 | 80 | def lookup(self, volumeId): 81 | url = 'http://{}/dir/lookup?volumeId={}'.format(self.master, volumeId) 82 | return self._request(url) 83 | 84 | def grow(self, count, replication): 85 | url = 'http://{}/dir/grow?count={}&replication={}'.format(self.master,int(count),replication) 86 | return self._request(url) 87 | 88 | def store(self, volume_host, fid, file = None, content = None, name = None, content_type = None): 89 | content_type, body = encode_upload(file=file, content=content, name=name, content_type=content_type) 90 | headers = { 'Content-Type': content_type } 91 | url = 'http://{}/{}'.format(volume_host, fid) 92 | first, second, result = self._request(url, 'POST', body=body, headers=headers) 93 | print type(result) 94 | print result 95 | if isinstance(result, dict): 96 | return result['size'] 97 | 98 | 99 | 100 | 101 | if __name__ == '__main__': 102 | import argparse 103 | parser = argparse.ArgumentParser(usage='%(prog)s command') 104 | parser.add_argument('-a','--assign',metavar='count',type=int) 105 | parser.add_argument('-l','--lookup',metavar='volumeId',type=int) 106 | parser.add_argument('-u','--upload',metavar='filename',type=str) 107 | parser.add_argument('-f','--fetch',metavar='host/fid',type=str) 108 | parser.add_argument('-r','--replication',metavar='type',type=str,default=None) 109 | parser.add_argument('-s','--status', action='store_true') 110 | args, remaining = parser.parse_known_args() 111 | 112 | client = WeedClient() 113 | if args.assign: 114 | ret = client.assign(args.assign,args.replication) 115 | print type(ret) 116 | print json.dumps(ret, indent=4) 117 | elif args.lookup: 118 | ret = client.lookup(args.lookup) 119 | print type(ret) 120 | print json.dumps(ret, indent=4) 121 | elif args.status: 122 | ret = client.status() 123 | print json.dumps(ret, indent=4) 124 | elif args.upload: 125 | volume_host, fid = client.assign(1,args.replication) 126 | ret = client.store(volume_host, fid, args.upload) 127 | #ret = client.store(volume_host, fid, open(args.upload, 'rb'), name=os.path.basename(args.upload)) 128 | print type(ret) 129 | print ret 130 | elif args.fetch: 131 | volume_host, fid = args.fetch.split('/', 2) 132 | ret = client.retrieve(volume_host, fid, head=True) 133 | print ret 134 | else: 135 | parser.print_help() 136 | -------------------------------------------------------------------------------- /sbin/server_image.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # tested at macosx 4 | 5 | ### BEGIN INIT INFO 6 | # Provides: uwsgi imsto 7 | # Required-Start: $remote_fs $network 8 | # Required-Stop: $remote_fs $network 9 | # Default-Start: 2 3 4 5 10 | # Default-Stop: 0 1 6 11 | # Short-Description: starts imsto 12 | # Description: starts imsto 13 | ### END INIT INFO 14 | 15 | name="imsto" 16 | 17 | PATH=/opt/local/bin:/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin:/usr/opt/bin 18 | 19 | uwsgi_BIN=`which uwsgi` 20 | uwsgi_uid="nobody" 21 | uwsgi_gid="nobody" 22 | 23 | uwsgi_pidfile="/var/run/${name}.pid" 24 | uwsgi_config="/etc/uwsgi_apps/${name}.ini" 25 | 26 | uwsgi_opts="--ini ${uwsgi_config} --pidfile ${uwsgi_pidfile}" 27 | 28 | if [ -z "${uwsgi_BIN}" ]; then 29 | echo "uwsgi not found or access denied" 30 | exit 1; 31 | fi 32 | 33 | 34 | case `echo "testing\c"`,`echo -n testing` in 35 | *c*,-n*) echo_n= echo_c= ;; 36 | *c*,*) echo_n=-n echo_c= ;; 37 | *) echo_n= echo_c='\c' ;; 38 | esac 39 | 40 | 41 | check_dirs () { 42 | 43 | if [ ! -e $uwsgi_config ]; then 44 | echo "${uwsgi_config} not found" 45 | exit 1; 46 | fi 47 | } 48 | 49 | wait_for_pid () { 50 | try=0 51 | 52 | while test $try -lt 5 ; do 53 | 54 | case "$1" in 55 | 'created') 56 | if [ -f "$2" ] ; then 57 | try='' 58 | break 59 | fi 60 | ;; 61 | 62 | 'removed') 63 | if [ ! -f "$2" ] ; then 64 | try='' 65 | break 66 | fi 67 | ;; 68 | esac 69 | 70 | echo $echo_n ".$echo_c" 71 | try=`expr $try + 1` 72 | sleep 1 73 | 74 | done 75 | 76 | } 77 | 78 | case "$1" in 79 | start) 80 | 81 | check_dirs 82 | 83 | echo $echo_n "Starting ${name} $echo_c" 84 | 85 | $uwsgi_BIN $uwsgi_opts 86 | 87 | if [ "$?" != 0 ] ; then 88 | echo " failed" 89 | exit 1 90 | fi 91 | 92 | wait_for_pid created $uwsgi_pidfile 93 | 94 | if [ -n "$try" ] ; then 95 | echo " failed" 96 | exit 1 97 | else 98 | echo " done" 99 | fi 100 | ;; 101 | 102 | stop) 103 | echo $echo_n "Gracefully shutting down ${name} $echo_c" 104 | 105 | if [ ! -r $uwsgi_pidfile ] ; then 106 | echo "warning, no pid file found - ${name} is not running ?" 107 | exit 1 108 | fi 109 | 110 | #kill -QUIT `cat $uwsgi_pidfile` 111 | $uwsgi_BIN --stop $uwsgi_pidfile 112 | 113 | wait_for_pid removed $uwsgi_pidfile 114 | 115 | if [ -n "$try" ] ; then 116 | echo " failed. Use force-quit" 117 | exit 1 118 | else 119 | echo " done" 120 | fi 121 | ;; 122 | 123 | force-quit) 124 | echo $echo_n "Terminating ${name} " 125 | 126 | if [ ! -r $uwsgi_pidfile ] ; then 127 | echo "warning, no pid file found - ${name} is not running ?" 128 | exit 1 129 | fi 130 | 131 | kill -KILL `cat $uwsgi_pidfile` 132 | 133 | wait_for_pid removed $uwsgi_pidfile 134 | 135 | if [ -n "$try" ] ; then 136 | echo " failed" 137 | exit 1 138 | else 139 | echo " done" 140 | fi 141 | ;; 142 | 143 | restart) 144 | $0 stop 145 | $0 start 146 | ;; 147 | 148 | reload) 149 | 150 | echo $echo_n "Reload service ${name} " 151 | 152 | if [ ! -r $uwsgi_pidfile ] ; then 153 | echo "warning, no pid file found - ${name} is not running ?" 154 | exit 1 155 | fi 156 | 157 | #kill -TERM `cat $uwsgi_pidfile` 158 | $uwsgi_BIN --reload $uwsgi_pidfile 159 | 160 | echo " done" 161 | ;; 162 | 163 | *) 164 | echo "Usage: $0 {start|stop|force-quit|restart|reload}" 165 | exit 1 166 | ;; 167 | 168 | esac 169 | -------------------------------------------------------------------------------- /sbin/server_man.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: uwsgi imsto 5 | # Required-Start: $remote_fs $network 6 | # Required-Stop: $remote_fs $network 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: starts imsto 10 | # Description: starts imsto 11 | ### END INIT INFO 12 | 13 | name="manage" 14 | uwsgi_module="imsto.handlers:AuthAdminHandle" 15 | uwsgi_socket="/tmp/imsto_man.sock" 16 | 17 | prefix=/opt/imsto 18 | #exec_prefix=${prefix} 19 | config="${prefix}/config" 20 | 21 | PATH=/opt/local/bin:/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin:/usr/opt/bin 22 | PYTHON_EGG_CACHE=${prefix}/cache/eggs 23 | 24 | uwsgi_BIN=`which uwsgi` 25 | uwsgi_pidfile="${prefix}/logs/${name}.pid" 26 | uwsgi_logfile="${prefix}/logs/${name}.log" 27 | uwsgi_flags="--pp ${prefix} --env IMSTO_CONF_DIR=${config} --vacuum -C -p 1 -M -t 20 --limit-as 32 -m -w ${uwsgi_module}" 28 | 29 | uwsgi_opts="--pidfile ${uwsgi_pidfile} -s ${uwsgi_socket} -d ${uwsgi_logfile} ${uwsgi_flags}" 30 | # --uid ${uwsgi_uid} --gid ${uwsgi_gid} 31 | 32 | #echo "${uwsgi_BIN}" 33 | if [ -z "${uwsgi_BIN}" ]; then 34 | echo "uwsgi not found or access denied" 35 | exit 1; 36 | fi 37 | 38 | 39 | case `echo "testing\c"`,`echo -n testing` in 40 | *c*,-n*) echo_n= echo_c= ;; 41 | *c*,*) echo_n=-n echo_c= ;; 42 | *) echo_n= echo_c='\c' ;; 43 | esac 44 | 45 | wait_for_pid () { 46 | try=0 47 | 48 | while test $try -lt 5 ; do 49 | 50 | case "$1" in 51 | 'created') 52 | if [ -f "$2" ] ; then 53 | try='' 54 | break 55 | fi 56 | ;; 57 | 58 | 'removed') 59 | if [ ! -f "$2" ] ; then 60 | try='' 61 | break 62 | fi 63 | ;; 64 | esac 65 | 66 | echo $echo_n ".$echo_c" 67 | try=`expr $try + 1` 68 | sleep 1 69 | 70 | done 71 | 72 | } 73 | 74 | case "$1" in 75 | start) 76 | 77 | echo $echo_n "Starting ${name} $echo_c" 78 | 79 | $uwsgi_BIN $uwsgi_opts 80 | 81 | if [ "$?" != 0 ] ; then 82 | echo " failed" 83 | exit 1 84 | fi 85 | 86 | wait_for_pid created $uwsgi_pidfile 87 | 88 | if [ -n "$try" ] ; then 89 | echo " failed" 90 | exit 1 91 | else 92 | echo " done" 93 | fi 94 | ;; 95 | 96 | stop) 97 | echo $echo_n "Gracefully shutting down ${name} $echo_c" 98 | 99 | if [ ! -r $uwsgi_pidfile ] ; then 100 | echo "warning, no pid file found - ${name} is not running ?" 101 | exit 1 102 | fi 103 | 104 | kill -QUIT `cat $uwsgi_pidfile` 105 | 106 | wait_for_pid removed $uwsgi_pidfile 107 | 108 | if [ -n "$try" ] ; then 109 | echo " failed. Use force-exit" 110 | exit 1 111 | else 112 | echo " done" 113 | fi 114 | ;; 115 | 116 | force-quit) 117 | echo $echo_n "Terminating ${name} " 118 | 119 | if [ ! -r $uwsgi_pidfile ] ; then 120 | echo "warning, no pid file found - ${name} is not running ?" 121 | exit 1 122 | fi 123 | 124 | kill -KILL `cat $uwsgi_pidfile` 125 | 126 | wait_for_pid removed $uwsgi_pidfile 127 | 128 | if [ -n "$try" ] ; then 129 | echo " failed" 130 | exit 1 131 | else 132 | echo " done" 133 | fi 134 | ;; 135 | 136 | restart) 137 | $0 stop 138 | $0 start 139 | ;; 140 | 141 | reload) 142 | 143 | echo $echo_n "Reload service ${name} $echo_c" 144 | 145 | if [ ! -r $uwsgi_pidfile ] ; then 146 | echo "warning, no pid file found - ${name} is not running ?" 147 | exit 1 148 | fi 149 | 150 | kill -TERM `cat $uwsgi_pidfile` 151 | 152 | echo " done" 153 | ;; 154 | 155 | *) 156 | echo "Usage: $0 {start|stop|force-quit|restart|reload}" 157 | exit 1 158 | ;; 159 | 160 | esac 161 | -------------------------------------------------------------------------------- /sbin/setup_sto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## $Id$ 4 | 5 | ROOTNAME="imsto" 6 | PATH=/opt/local/bin:/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin 7 | 8 | OS=`uname -s` 9 | echo $OS 10 | if [ $OS = "Darwin" ]; then 11 | PREFIX=/opt 12 | else 13 | PREFIX=/var 14 | fi 15 | 16 | if [ ! -d $PREFIX ]; then 17 | echo "$PREFIX not exists" 18 | exit 1; 19 | fi 20 | 21 | if [ ! -e ${PREFIX}/$ROOTNAME ]; then 22 | echo "path: ${PREFIX}/$ROOTNAME not exists" 23 | exit 1; 24 | fi 25 | 26 | STO_DIR=${PREFIX}/$ROOTNAME 27 | CACHE_DIR=${STO_DIR}/cache 28 | 29 | ## Cache 30 | [ ! -d $CACHE_DIR -a ! -e $CACHE_DIR ] && mkdir $CACHE_DIR 31 | if [ -d $CACHE_DIR ]; then 32 | chmod a+w $CACHE_DIR 33 | ## Cache: dirs 34 | for dir in thumb images eggs 35 | do 36 | [ -e $CACHE_DIR/$dir ] || mkdir $CACHE_DIR/$dir 37 | if [ -d $CACHE_DIR/$dir ]; then 38 | chmod a+w $CACHE_DIR/$dir 39 | fi 40 | done 41 | fi 42 | 43 | 44 | -------------------------------------------------------------------------------- /sbin/uwsgi.init.gentoo.sh: -------------------------------------------------------------------------------- 1 | #!/sbin/runscript 2 | 3 | # uwsgi boot script for gentoo only 4 | # install: 5 | # - sudo cp uwsgi.init.gentoo.sh /etc/init.d/uwsgi 6 | # - cd /etc/init.d 7 | # - sudo chmod a+x uwsgi 8 | # - sudo ln -s uwsgi myapp 9 | # - cd /etc/uwsgi_apps 10 | # - sudo vim myapp.ini 11 | # author: liut, 20121210 12 | 13 | 14 | uwsgi_config="/etc/uwsgi_apps/${SVCNAME}.ini" 15 | 16 | command=/usr/local/bin/uwsgi 17 | pidfile=/var/run/${SVCNAME}.pid 18 | command_args="--uid nobody --ini ${uwsgi_config} --pidfile ${pidfile}" 19 | 20 | extra_started_commands="reload" 21 | 22 | depend() { 23 | need net 24 | } 25 | 26 | start() { 27 | ebegin "Starting ${SVCNAME}" 28 | ${command} ${command_args} &>/dev/null 29 | eend $? "Failed to start ${SVCNAME}" 30 | } 31 | 32 | stop() { 33 | ebegin "Stopping ${SVCNAME}" 34 | ${command} --stop ${pidfile} &>/dev/null 35 | eend $? "Failed to stop ${SVCNAME}" 36 | } 37 | 38 | reload() { 39 | ebegin "Refreshing ${SVCNAME} configuration" 40 | ${command} --reload ${pidfile} &>/dev/null 41 | eend $? "Failed to reload ${SVCNAME}" 42 | } 43 | 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | from imsto import __version__, __author__, __author_email__ 4 | 5 | # Utility function to read the README file. 6 | # Used for the long_description. It's nice, because now 1) we have a top level 7 | # README file and 2) it's easier to type in the README file than to put a raw 8 | # string in below ... 9 | def read(fname): 10 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | 12 | CLASSIFIERS = [ 13 | "Development Status :: 3 - Alpha", 14 | "Environment :: Console", 15 | "Environment :: Web Environment", 16 | "Intended Audience :: Customer Service", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: BSD License", 19 | "Operating System :: MacOS :: MacOS X", 20 | "Operating System :: POSIX :: Linux", 21 | "Programming Language :: JavaScript", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 2.7", 24 | "Topic :: Communications :: File Sharing", 25 | "Topic :: Internet", 26 | "Topic :: Multimedia :: Graphics", 27 | "Topic :: Utilities", 28 | ] 29 | 30 | setup( 31 | name = "imsto", 32 | version = __version__, 33 | author = __author__, 34 | author_email = __author_email__, 35 | description = ("a little image storage"), 36 | license = "BSD", 37 | keywords = "imsto image storage", 38 | url = "http://github.com/liut/imsto", 39 | packages=['imsto', 'imsto.django'], 40 | install_requires = ['pymongo>=2.5'], 41 | long_description = read('README.md'), 42 | classifiers = CLASSIFIERS, 43 | ) 44 | 45 | -------------------------------------------------------------------------------- /static/admin/css/base.css: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | /** 清除内外边距 **/ 3 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote /* structural elements 结构元素 */ 4 | ,dl, dt, dd, ul, ol, li /* list elements 列表元素 */ 5 | ,pre /* text formatting elements 文本格式元素 */ 6 | ,form, fieldset, legend, button, input, textarea /* form elements 表单元素 */ 7 | { /* ,th, td table elements 表格元素 */ 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | /** 设置默认字体 **/ 13 | body { 14 | /*font: 12px/1 Tahoma, Helvetica, Arial, "宋体", sans-serif;*/ 15 | font: 75%/1 Helvetica, Tahoma, Arial, "\5fae\8f6f\96c5\9ed1", "\5b8b\4f53", sans-serif; /* 用 ascii 字符表示,使得在任何编码下都无问题 */ 16 | } 17 | 18 | span.tip_text{font-size: .8em; line-height: 1.2em; padding: 2px; color: gray;} 19 | form input[type=text], 20 | form input[type=password], 21 | form select, 22 | form textarea{ 23 | padding: 3px 1px 2px 3px;border: 1px solid #333; 24 | border-radius:3px; -moz-border-radius:3px; -webkit-border-radius:3px; 25 | } 26 | form input[type=text]:focus, 27 | form input[type=password]:focus, 28 | form select:focus, 29 | form textarea:focus{ 30 | border-color:rgba(204,85,0,0.75); 31 | box-shadow: 0 0 .5em rgba(238,85,0,0.5);-moz-box-shadow: 0 0 .5em rgba(238,85,0,0.5);-webkit-box-shadow: 0 0 .5em #E50; 32 | } 33 | 34 | 35 | #rsp-status {position:absolute; z-index: 5; left:76px; top: 16px;} 36 | 37 | -------------------------------------------------------------------------------- /static/admin/css/colorbox.css: -------------------------------------------------------------------------------- 1 | /* 2 | ColorBox Core Style 3 | The following rules are the styles that are consistant between themes. 4 | Avoid changing this area to maintain compatability with future versions of ColorBox. 5 | */ 6 | #colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;} 7 | #cboxOverlay{position:fixed; width:100%; height:100%;} 8 | #cboxMiddleLeft, #cboxBottomLeft{clear:left;} 9 | #cboxContent{position:relative; overflow:visible;} 10 | #cboxLoadedContent{overflow:auto;} 11 | #cboxLoadedContent iframe{display:block; width:100%; height:100%; border:0;} 12 | #cboxTitle{margin:0;} 13 | #cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%;} 14 | #cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;} 15 | 16 | /* 17 | ColorBox example user style 18 | The following rules are ordered and tabbed in a way that represents the 19 | order/nesting of the generated HTML, so that the structure easier to understand. 20 | */ 21 | #cboxOverlay{background:#fff;} 22 | 23 | #colorbox{} 24 | #cboxContent{margin-top:32px;} 25 | #cboxLoadedContent{background:#000; padding:1px;} 26 | #cboxLoadingGraphic{background:url(images/loading.gif) no-repeat center center;} 27 | #cboxLoadingOverlay{background:#000;} 28 | #cboxTitle{position:absolute; top:-22px; left:0; color:#000;} 29 | #cboxCurrent{position:absolute; top:-22px; right:205px; text-indent:-9999px;} 30 | #cboxSlideshow, #cboxPrevious, #cboxNext, #cboxClose{text-indent:-9999px; width:20px; height:20px; position:absolute; top:-20px; background:url(images/controls.png) no-repeat 0 0;} 31 | #cboxPrevious{background-position:0px 0px; right:44px;} 32 | #cboxPrevious.hover{background-position:0px -25px;} 33 | #cboxNext{background-position:-25px 0px; right:22px;} 34 | #cboxNext.hover{background-position:-25px -25px;} 35 | #cboxClose{background-position:-50px 0px; right:0;} 36 | #cboxClose.hover{background-position:-50px -25px;} 37 | .cboxSlideshow_on #cboxPrevious, .cboxSlideshow_off #cboxPrevious{right:66px;} 38 | .cboxSlideshow_on #cboxSlideshow{background-position:-75px -25px; right:44px;} 39 | .cboxSlideshow_on #cboxSlideshow.hover{background-position:-100px -25px;} 40 | .cboxSlideshow_off #cboxSlideshow{background-position:-100px 0px; right:44px;} 41 | .cboxSlideshow_off #cboxSlideshow.hover{background-position:-75px -25px;} 42 | -------------------------------------------------------------------------------- /static/admin/css/images.css: -------------------------------------------------------------------------------- 1 | 2 | .info { 3 | position:absolute; 4 | top:0; 5 | left:0; 6 | max-height: 38px; 7 | padding:10px 15px; 8 | } 9 | .box{ 10 | width:600px;margin:auto;padding:.8em; 11 | text-shadow: black 0px 1px 1px; 12 | } 13 | .box label{float: left;} 14 | 15 | #image-list {/*-moz-column-count: 3;*/} 16 | #image-list li{ 17 | min-width: 112px; max-width: 180px; height: 178px; margin: .8em; margin-right: 1.8em; margin-bottom: 1.8em; padding: .5em; 18 | display: block; float: left; text-align: center; 19 | border: 1px solid rgba(60, 60, 60, .2); 20 | -moz-box-shadow: 0 1px 5px 1px rgba(80, 80, 80, .2); -webkit-box-shadow: 0 1px 5px rgba(80, 80, 80, .2); box-shadow: 0px 1px 5px 1px rgba(80, 80, 80, .2); 21 | -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; 22 | } 23 | #image-list li:hover{border-color:#999;border-color:rgba(200,80,20,.5);-moz-box-shadow:0 0 5px 1px rgba(200,80,20,.5);-webkit-box-shadow:0 0 5px rgba(200,80,20,.5);} 24 | #image-list li.new{-moz-box-shadow: 1px 3px 6px 1px rgba(200, 80, 80, .4);} 25 | 26 | #image-list li img { display: block; 27 | max-width: 160px; max-height: 160px; margin: auto; border: 1px solid Gray; 28 | } 29 | #image-list li .lbl {float: left; margin-top:5px; max-width: 150px;} 30 | #image-list li .btn {float: right; margin-top: 3px;cursor: pointer;} 31 | .throbber { 32 | position: absolute; 33 | z-index: 99999; 34 | } 35 | 36 | input { 37 | display: block; 38 | margin: auto; 39 | } 40 | button { 41 | float: right; 42 | } 43 | #buttons-panel{min-height: 3em;} 44 | #buttons-panel .btn{min-height: 1.5em; margin: .4em .2em .2em 2em;float:left;} 45 | #progressbar{width: 50%;float:left;} 46 | 47 | pre{display:block;margin:auto;} 48 | 49 | .button{ margin: .8em .5em .5em 1.5em;} 50 | .button_box{ display:inline-block; } 51 | .button_box2{ display:inline-block; margin:auto;} 52 | 53 | #pager { float: right; margin: 0 1em; } 54 | #head {min-height: 52px;} 55 | #rsp-status { left: 34%; top: 30px; } 56 | 57 | -------------------------------------------------------------------------------- /static/admin/css/images/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liut/imsto-python-old/987c235690d6eefe9a5b47c8e348798e278999aa/static/admin/css/images/controls.png -------------------------------------------------------------------------------- /static/admin/css/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liut/imsto-python-old/987c235690d6eefe9a5b47c8e348798e278999aa/static/admin/css/images/loading.gif -------------------------------------------------------------------------------- /static/admin/css/pager.css: -------------------------------------------------------------------------------- 1 | ul.pages { 2 | display:block; 3 | border:none; 4 | text-transform:uppercase; 5 | font-size:10px; 6 | margin:10px 0 40px; 7 | padding:0; 8 | } 9 | 10 | ul.pages li { 11 | list-style:none; 12 | float:left; 13 | border:1px solid #ccc; 14 | text-decoration:none; 15 | margin:0 5px 0 0; 16 | padding:5px 6px; 17 | } 18 | 19 | ul.pages li:hover { 20 | border:1px solid #003f7e; 21 | } 22 | 23 | ul.pages li.empty { 24 | border:1px solid #eee; 25 | color:#eee; 26 | } 27 | 28 | ul.pages li.current { 29 | border:1px solid #003f7e; 30 | color:#000; 31 | font-weight:700; 32 | background-color:#eee; 33 | } -------------------------------------------------------------------------------- /static/admin/css/upload.css: -------------------------------------------------------------------------------- 1 | 2 | .box{ 3 | width:600px;margin:auto;padding:.8em; 4 | text-shadow: black 0px 1px 1px; 5 | } 6 | .box label{float: left;} 7 | #dropbox { 8 | display: block; 9 | margin: auto; 10 | z-index: 9999; 11 | margin-top: 10px; 12 | } 13 | 14 | #dropbox path { 15 | stroke: #aaa; 16 | stroke-width: 10px; 17 | stroke-linejoin: round; 18 | fill: rgba(0, 0, 0, 0.5); 19 | stroke-dasharray: 42, 4; 20 | } 21 | 22 | #dropbox text { 23 | fill: white; 24 | font-size: 20px; 25 | text-anchor: middle; 26 | } 27 | 28 | #dropbox[dragenter="true"] { 29 | -moz-transform: scale(2, 1.5); 30 | } 31 | 32 | 33 | #image-pic-list {/*-moz-column-count: 3;*/ 34 | clear: both; 35 | } 36 | #image-pic-list li{ 37 | min-width: 120px; max-width: 240px; height: 220px; margin: .8em; margin-right: 1.8em; margin-bottom: 1.8em; padding: .5em; 38 | display: block; float: left; text-align: center; 39 | -moz-box-shadow: 0 0 6px 4px rgba(80, 80, 80, 0.2); 40 | } 41 | #image-pic-list li.new{-moz-box-shadow: 0 0 6px 4px rgba(200, 80, 80, 0.4);} 42 | 43 | #image-pic-list li img { display: block; 44 | max-width: 200px; max-height: 180px; margin: auto; border: 1px solid Gray; 45 | } 46 | #image-pic-list li .lbl {float: left; margin-top:5px; max-width: 150px;} 47 | #image-pic-list li .btn {float: right; margin-top: 6px;cursor: pointer;} 48 | .throbber { 49 | position: absolute; 50 | z-index: 99999; 51 | } 52 | 53 | input { 54 | display: block; 55 | margin: auto; 56 | } 57 | button { 58 | float: right; 59 | } 60 | #buttons-panel{min-height: 3em; clear: both;} 61 | #buttons-panel .btn{min-height: 1.5em; margin: .4em .2em .2em 2em;float:left;} 62 | #progressbar{width: 50%;float:left;} 63 | 64 | pre{display:block;margin:auto;} 65 | 66 | fieldset { 67 | padding:10px; 68 | margin:10px; 69 | width:500px; 70 | color:#333; 71 | border:#ccc solid 0.5px; 72 | } 73 | legend { 74 | color:#06c; 75 | font-weight:800; 76 | background:#fff; 77 | border:#b6b6b6 solid 1px; 78 | padding:5px 12px; 79 | } 80 | 81 | .nav{ 82 | float: left; 83 | } 84 | .button{ margin: .5em .5em .5em 1.5em;} 85 | 86 | -------------------------------------------------------------------------------- /static/admin/gallery.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | imsto manage 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 131 | 132 | 133 | 134 | 135 | 136 | 146 |
147 | 148 |
    149 |
150 | 151 |
152 | 153 | 154 | -------------------------------------------------------------------------------- /static/admin/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | imsto demo 7 | 8 | 12 | 13 | 14 | 15 |

ImSto demos:

16 | 17 | Gallery 18 | Upload 19 | 20 |
    21 |
  • Upload only support Firefox 3.5+ for now
  • 22 |
  • The demo allowed max_file_size < 100Kb
  • 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /static/admin/js/dust_func.js: -------------------------------------------------------------------------------- 1 | 2 | if (!this.Dust) { 3 | var Dust = {}; 4 | } 5 | (function($$, $){ 6 | $$.extend = function (destination, source, callback) { 7 | for (var property in source) 8 | destination[property] = source[property]; 9 | if($$.dev) destination['__noSuchMethod__'] = function (prop, args){ error(prop, " : no such method exists", args); }; 10 | if ($.isFunction(callback)) callback(); 11 | return destination; 12 | }; 13 | $$.cache = {}; 14 | $$.extend(window, { 15 | log: ($$.dev && window.console) ? function() { console.log.apply(console, arguments); } : function() { }, 16 | error: ($$.dev && window.console) ? function() { console.error.apply(console, arguments); } : function() { }, 17 | dir: ($$.dev && window.console) ? function(a) { console.dir(a); } : function() { }, 18 | info: ($$.dev && window.console) ? function(a) { console.info(a); } : function() { }, 19 | }, function(){ 20 | log("logging enabled"); 21 | log("Window object extended"); 22 | }); 23 | 24 | 25 | $$.extend($$, { 26 | 27 | alert: function(message, title, callback){ 28 | if ($.isFunction(title)) { 29 | callback = title; 30 | title = false; 31 | } 32 | title = title || "提示!"; 33 | $('body').append(''); 35 | $("#dialog").dialog({ 36 | bgiframe: false, 37 | modal: false, 38 | closeOnEscape: true, 39 | close: function(){ 40 | $('#dialog').remove(); 41 | if ($.isFunction(callback)) callback(); 42 | }, 43 | buttons: { 44 | Ok: function() { 45 | $(this).dialog('close'); 46 | } 47 | } 48 | }); 49 | }, 50 | confirm: function(title, message, callback, falseCallBack, height, width){ 51 | if(title == null) title = "确认"; 52 | if(message == null) message = "Are you sure want to proceed ?"; 53 | $('body').append(''); 55 | $("#dialog").dialog({ 56 | bgiframe: true, 57 | resizable: false, 58 | height: height, 59 | width: width, 60 | modal: false, 61 | overlay: { 62 | backgroundColor: '#000', 63 | opacity: 0.5 64 | }, 65 | closeOnEscape: true, 66 | dialogClass: 'confirm', 67 | close: function(){ 68 | $(this).dialog('destroy'); 69 | $('#dialog').remove(); 70 | }, 71 | buttons: { 72 | 'Ok': function() { 73 | $(this).dialog('destroy'); 74 | $('#dialog').remove(); 75 | if ($.isFunction(callback)) callback(); 76 | return true; 77 | }, 78 | 'Cancel': function() { 79 | $(this).dialog('destroy'); 80 | $('#dialog').remove(); 81 | if ($.isFunction(falseCallBack)) falseCallBack(); 82 | return false; 83 | } 84 | } 85 | }); 86 | }, 87 | prompt: function(title, message, default_value,callback, optional_message){ 88 | optional_message = optional_message || ""; 89 | if (title == null || message == null || callback == null) 90 | return false; 91 | default_value = default_value || ""; 92 | $('body').append(''); 94 | if (optional_message.blank()) optional_message = null; 95 | $("#dialog").dialog({ 96 | bgiframe: true, 97 | resizable: false, 98 | height: optional_message == null ? 300 : 180, 99 | width: optional_message == null ? 300 : 500, 100 | modal: false, 101 | overlay: { 102 | backgroundColor: '#000', 103 | opacity: 0.5 104 | }, 105 | closeOnEscape: true, 106 | dialogClass: 'confirm', 107 | close: function(){ 108 | $(this).dialog('destroy'); 109 | $('#dialog').remove(); 110 | }, 111 | buttons: { 112 | 'Ok': function() { 113 | var value = $('#prompt_value').val(); 114 | $(this).dialog('destroy'); 115 | $('#dialog').remove(); 116 | if ($.isFunction(callback)) callback(value); 117 | return true; 118 | }, 119 | 'Close': function() { 120 | $(this).dialog('destroy'); 121 | $('#dialog').remove(); 122 | return false; 123 | } 124 | } 125 | }); 126 | }, 127 | splitAjaxResult: function(data) { //console.log(data, typeof data); 128 | if (typeof data === "undefined" || data === null) { 129 | return "操作完成,但返回结果为空"; 130 | } 131 | var msg, lb_true = "操作成功!", lb_false = "操作失败!!", lb_errno = "\n返回代码: ", lb_error = "\n返回说明: "; 132 | if (typeof data == "boolean" || typeof data == "string" || typeof data == "number") { 133 | msg = data ? lb_true : lb_false; 134 | } 135 | else if ($.isArray(data) && data.length > 0) { 136 | msg = data[0] ? lb_true : lb_false; 137 | if (data.length > 1) { 138 | msg += lb_errno + data[1]; 139 | if (data.length > 2) msg += lb_error + data[2]; 140 | } 141 | } 142 | else if(data && data.success == true || parseInt(data) > 0){ 143 | msg = lb_true; 144 | if (typeof data.message === "string") { 145 | msg += "\n返回消息:" + data.message; 146 | } 147 | }else{ 148 | msg = lb_false; 149 | if(typeof data.errors === "object") { 150 | if(data.errors.code) msg += lb_errno + data.errors.code; 151 | if(data.errors.reason) msg += lb_error + data.errors.reason; 152 | } 153 | else { 154 | if (data.errno && data.errno > 0) { 155 | msg += lb_errno + data.errno; 156 | } 157 | if (data.error && data.error !== "") { 158 | msg += lb_error + data.error; 159 | } 160 | } 161 | } 162 | return msg; 163 | } 164 | }, function (){ 165 | log("$$ object extended"); 166 | }); 167 | 168 | 169 | })(Dust, jQuery); 170 | 171 | 172 | /** 173 | * 处理ajax操作的返回结果 174 | * 返回结果的格式 175 | * { 176 | * success: true or false, 177 | * errors: { code: 'error_code', reason: 'error_reason'}, 178 | * } 179 | * 180 | */ 181 | function alertAjaxResult(data, callback) 182 | { 183 | if(typeof data === "string" && data !== "") data = JSON.parse(data); 184 | try{ 185 | log(data, typeof data, typeof data.success); 186 | } catch(e){} 187 | var msg = ''; 188 | if ($.isArray(data)) { 189 | $.each(data, function(i, item){ 190 | msg += (i+1) + ": " + Dust.splitAjaxResult(item) + "\n"; 191 | }); 192 | } else { 193 | msg += Dust.splitAjaxResult(data); 194 | } 195 | Dust.alert(msg, '', callback); 196 | } 197 | 198 | (function($) { 199 | /** 200 | * example:
201 | * $('#land_box').radioButtons({data: Catalog.all.land, name: 'land'}); 202 | */ 203 | $.fn.radioButtons = function(options) { 204 | options = $.extend({ 205 | data: {'0':'Choice 1','1':'Choice 2'}, 206 | name: 'radio', 207 | selected: '', 208 | skipEmpty: true 209 | }, options); 210 | var self = $(this).empty(), i = 0; 211 | for( var k in options.data) { 212 | if (options.skipEmpty && k === "") continue; 213 | var id = 'ws_'+options.name+'_'+i, text = options.data[k], radio = 214 | $("").attr("id", id) 215 | .attr("type", "radio").attr("name", options.name) 216 | .attr("value", k); 217 | if (k === options.selected) radio.attr('checked', 'checked'); 218 | radio.appendTo(self); 219 | $("