├── LICENSE.txt ├── admin.py ├── blog.py ├── common.py ├── common.pyc ├── config.yaml ├── favicon.ico ├── index.wsgi ├── model.py ├── model.pyc ├── qiniu ├── __init__.py ├── auth │ ├── __init__.py │ ├── digest.py │ └── up.py ├── conf.py ├── fop.py ├── httplib_chunk.py ├── io.py ├── resumable_io.py ├── rpc.py ├── rs │ ├── __init__.py │ ├── rs.py │ ├── rs_token.py │ └── test │ │ ├── __init__.py │ │ ├── rs_test.py │ │ └── rs_token_test.py ├── rsf.py └── test │ ├── __init__.py │ ├── conf_test.py │ ├── fop_test.py │ ├── io_test.py │ ├── resumable_io_test.py │ ├── rpc_test.py │ └── rsf_test.py ├── readme ├── readme.txt ├── setting.py ├── setting.pyc ├── static ├── admin │ └── style.css ├── facefiles │ ├── b.png │ ├── bl.png │ ├── br.png │ ├── closelabel.gif │ ├── facebox.css │ ├── facebox.js │ ├── loading.gif │ ├── tl.png │ └── tr.png ├── highlight │ ├── highlight.min.js │ └── sunburst.min.css ├── img │ └── grey.gif ├── jquery-plugin │ ├── jquery-1.6.4.js │ ├── jquery.cookie.js │ └── jquery.upload-1.0.2.min.js ├── markitup │ ├── jquery.markitup.js │ ├── sets │ │ └── html │ │ │ ├── images │ │ │ ├── bold.png │ │ │ ├── clean.png │ │ │ ├── code.png │ │ │ ├── colors.png │ │ │ ├── cut.png │ │ │ ├── h1.png │ │ │ ├── h2.png │ │ │ ├── h3.png │ │ │ ├── h4.png │ │ │ ├── h5.png │ │ │ ├── h6.png │ │ │ ├── italic.png │ │ │ ├── link.png │ │ │ ├── list-bullet.png │ │ │ ├── list-item.png │ │ │ ├── list-numeric.png │ │ │ ├── paragraph.png │ │ │ ├── picture.png │ │ │ ├── preview.png │ │ │ ├── quotes.png │ │ │ └── stroke.png │ │ │ ├── set.js │ │ │ └── style.css │ ├── skins │ │ └── simple │ │ │ ├── images │ │ │ ├── handle.png │ │ │ ├── menu.png │ │ │ └── submenu.png │ │ │ └── style.css │ └── templates │ │ ├── preview.css │ │ └── preview.html └── themes │ ├── default │ └── style.css │ ├── octopress-disqus │ ├── css │ │ └── screen.css │ ├── images │ │ ├── bird_32_gray.png │ │ ├── bird_32_gray_fail.png │ │ ├── code_bg.png │ │ ├── dotted-border.png │ │ ├── email.png │ │ ├── line-tile.png │ │ ├── noise.png │ │ ├── rss.png │ │ └── search.png │ └── js │ │ └── main.js │ └── octopress │ ├── css │ └── screen.css │ ├── images │ ├── bird_32_gray.png │ ├── bird_32_gray_fail.png │ ├── code_bg.png │ ├── dotted-border.png │ ├── email.png │ ├── line-tile.png │ ├── noise.png │ ├── rss.png │ └── search.png │ └── js │ └── main.js ├── templates ├── admin │ ├── _layout_admin.html │ ├── admin_addpost.html │ ├── admin_comment.html │ ├── admin_editpost.html │ ├── admin_flushdata.html │ ├── admin_index.html │ ├── admin_link.html │ ├── admin_login.html │ └── error.html ├── default │ ├── _layout.html │ ├── comment.html │ ├── comments.html │ ├── index.html │ ├── links.html │ ├── page.html │ ├── page_pw.html │ └── sider.html ├── index.xml ├── octopress-disqus │ ├── _layout.html │ ├── index.html │ ├── page.html │ └── sider.html ├── octopress │ ├── _layout.html │ ├── comment.html │ ├── comments.html │ ├── index.html │ ├── newcomment.html │ ├── page.html │ └── sider.html ├── robots.txt ├── rpc.xml └── sitemap.html ├── tenjin.py └── tenjin.pyc /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 ego008 2 | 3 | https://github.com/ego008/saepy-log 4 | http://code.google.com/p/sae-python-tornado-blog/ 5 | 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #import logging 3 | import re 4 | import os.path 5 | from traceback import format_exc 6 | from urllib import unquote, quote, urlencode 7 | from urlparse import urljoin, urlunsplit 8 | 9 | from datetime import datetime, timedelta 10 | 11 | import tenjin 12 | from tenjin.helpers import * 13 | 14 | from setting import * 15 | 16 | import tornado.web 17 | 18 | 19 | #Memcache 是否可用、用户是否在后台初始化Memcache 20 | MC_Available = False 21 | if PAGE_CACHE: 22 | import pylibmc 23 | mc = pylibmc.Client() #只需初始化一次? 24 | try: 25 | MC_Available = mc.set('mc_available', '1', 3600) 26 | except: 27 | pass 28 | 29 | ##### 30 | def slugfy(text, separator='-'): 31 | text = text.lower() 32 | text = re.sub("[¿_\- ,。:;‘“’”【】『』§!-——+◎#¥%……※×()《》?、÷]+", ' ', text) 33 | ret_list = [] 34 | for c in text: 35 | ordnum = ord(c) 36 | if 47",">") 51 | 52 | def safe_decode(con): 53 | return con.replace("<","<").replace(">",">") 54 | 55 | def unquoted_unicode(string, coding='utf-8'): 56 | return unquote(string).decode(coding) 57 | 58 | def quoted_string(unicode, coding='utf-8'): 59 | return quote(unicode.encode(coding)) 60 | 61 | def cnnow(): 62 | return datetime.utcnow() + timedelta(hours =+ 8) 63 | 64 | # get time_from_now 65 | def timestamp_to_datetime(timestamp): 66 | return datetime.fromtimestamp(timestamp) 67 | 68 | def time_from_now(time): 69 | if isinstance(time, int): 70 | time = timestamp_to_datetime(time) 71 | #time_diff = datetime.utcnow() - time 72 | time_diff = cnnow() - time 73 | days = time_diff.days 74 | if days: 75 | if days > 730: 76 | return '%s years ago' % (days / 365) 77 | if days > 365: 78 | return '1 year ago' 79 | if days > 60: 80 | return '%s months ago' % (days / 30) 81 | if days > 30: 82 | return '1 month ago' 83 | if days > 14: 84 | return '%s weeks ago' % (days / 7) 85 | if days > 7: 86 | return '1 week ago' 87 | if days > 1: 88 | return '%s days ago' % days 89 | return '1 day ago' 90 | seconds = time_diff.seconds 91 | if seconds > 7200: 92 | return '%s hours ago' % (seconds / 3600) 93 | if seconds > 3600: 94 | return '1 hour ago' 95 | if seconds > 120: 96 | return '%s minutes ago' % (seconds / 60) 97 | if seconds > 60: 98 | return '1 minute ago' 99 | if seconds > 1: 100 | return '%s seconds ago' %seconds 101 | return '%s second ago' % seconds 102 | 103 | def clear_cache_by_pathlist(pathlist = []): 104 | if pathlist and MC_Available: 105 | try: 106 | mc = pylibmc.Client() 107 | mc.delete_multi([str(p) for p in pathlist]) 108 | except: 109 | pass 110 | 111 | def clear_all_cache(): 112 | if PAGE_CACHE: 113 | try: 114 | mc = pylibmc.Client() 115 | mc.flush_all() 116 | except: 117 | pass 118 | else: 119 | pass 120 | 121 | def format_date(dt): 122 | return dt.strftime('%a, %d %b %Y %H:%M:%S GMT') 123 | 124 | 125 | def memcached(key, cache_time=0, key_suffix_calc_func=None): 126 | def wrap(func): 127 | def cached_func(*args, **kw): 128 | if not MC_Available: 129 | return func(*args, **kw) 130 | 131 | key_with_suffix = key 132 | if key_suffix_calc_func: 133 | key_suffix = key_suffix_calc_func(*args, **kw) 134 | if key_suffix is not None: 135 | key_with_suffix = '%s:%s' % (key, key_suffix) 136 | 137 | mc = pylibmc.Client() 138 | value = mc.get(key_with_suffix) 139 | if value is None: 140 | value = func(*args, **kw) 141 | try: 142 | mc.set(key_with_suffix, value, cache_time) 143 | except: 144 | pass 145 | return value 146 | return cached_func 147 | return wrap 148 | 149 | RQT_RE = re.compile('\d*', re.I) 150 | def pagecache(key="", time=PAGE_CACHE_TIME, key_suffix_calc_func=None): 151 | def _decorate(method): 152 | def _wrapper(*args, **kwargs): 153 | if not MC_Available: 154 | method(*args, **kwargs) 155 | return 156 | 157 | req = args[0] 158 | 159 | key_with_suffix = key 160 | if key_suffix_calc_func: 161 | key_suffix = key_suffix_calc_func(*args, **kwargs) 162 | if key_suffix: 163 | key_with_suffix = '%s:%s' % (key, quoted_string(key_suffix)) 164 | 165 | if key_with_suffix: 166 | key_with_suffix = str(key_with_suffix) 167 | else: 168 | key_with_suffix = req.request.path 169 | 170 | mc = pylibmc.Client() 171 | html = mc.get(key_with_suffix) 172 | request_time = int(req.request.request_time()*1000) 173 | if html: 174 | req.write(html) 175 | #req.write(RQT_RE.sub('%d'%request_time, html)) 176 | else: 177 | result = method(*args, **kwargs) 178 | mc.set(key_with_suffix, result, time) 179 | return _wrapper 180 | return _decorate 181 | 182 | ### 183 | engine = tenjin.Engine(path=[os.path.join('templates', theme) for theme in [THEME,'admin']] + ['templates'], cache=tenjin.MemoryCacheStorage(), preprocess=True) 184 | class BaseHandler(tornado.web.RequestHandler): 185 | 186 | def render(self, template, context=None, globals=None, layout=False): 187 | if context is None: 188 | context = {} 189 | context.update({ 190 | 'request':self.request, 191 | }) 192 | return engine.render(template, context, globals, layout) 193 | 194 | def echo(self, template, context=None, globals=None, layout=False): 195 | self.write(self.render(template, context, globals, layout)) 196 | 197 | def set_cache(self, seconds, is_privacy=None): 198 | if seconds <= 0: 199 | self.set_header('Cache-Control', 'no-cache') 200 | #self.set_header('Expires', 'Fri, 01 Jan 1990 00:00:00 GMT') 201 | else: 202 | if is_privacy: 203 | privacy = 'public, ' 204 | elif is_privacy is None: 205 | privacy = '' 206 | else: 207 | privacy = 'private, ' 208 | self.set_header('Cache-Control', '%smax-age=%s' % (privacy, seconds)) 209 | 210 | def authorized(url='/admin/login'): 211 | def wrap(handler): 212 | def authorized_handler(self, *args, **kw): 213 | request = self.request 214 | user_name_cookie = self.get_cookie('username','') 215 | user_pw_cookie = self.get_cookie('userpw','') 216 | if user_name_cookie and user_pw_cookie: 217 | from model import User 218 | user = User.check_user(user_name_cookie, user_pw_cookie) 219 | else: 220 | user = False 221 | if request.method == 'GET': 222 | if not user: 223 | self.redirect(url) 224 | return False 225 | else: 226 | handler(self, *args, **kw) 227 | else: 228 | if not user: 229 | self.error(403) 230 | else: 231 | handler(self, *args, **kw) 232 | return authorized_handler 233 | return wrap 234 | 235 | def client_cache(seconds, privacy=None): 236 | def wrap(handler): 237 | def cache_handler(self, *args, **kw): 238 | self.set_cache(seconds, privacy) 239 | return handler(self, *args, **kw) 240 | return cache_handler 241 | return wrap 242 | -------------------------------------------------------------------------------- /common.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/common.pyc -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: saepy 3 | version: 1 4 | 5 | handle: 6 | - expire: if(path ~ "(.*)\.(js|css|jpg|png|gif|ico)") time 31536000 7 | - compress: if(out_header["Content-Length"] >= 1240) compress 8 | - hostaccess: if(path ~ "/task/") allow "10.0.0.0/8" 9 | ... 10 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/favicon.ico -------------------------------------------------------------------------------- /index.wsgi: -------------------------------------------------------------------------------- 1 | import sae 2 | import tornado.wsgi 3 | 4 | from blog import urls as blogurls 5 | from admin import urls as adminurls 6 | 7 | settings = { 8 | 'debug': True, 9 | } 10 | 11 | app = tornado.wsgi.WSGIApplication(blogurls + adminurls, **settings) 12 | 13 | application = sae.create_wsgi_app(app) 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /model.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/model.pyc -------------------------------------------------------------------------------- /qiniu/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Qiniu Resource Storage SDK for Python 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | For detailed document, please see: 6 | 7 | ''' 8 | 9 | # -*- coding: utf-8 -*- 10 | __version__ = '6.1.2' 11 | -------------------------------------------------------------------------------- /qiniu/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/qiniu/auth/__init__.py -------------------------------------------------------------------------------- /qiniu/auth/digest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from urlparse import urlparse 3 | import hmac 4 | from hashlib import sha1 5 | from base64 import urlsafe_b64encode 6 | 7 | from .. import rpc 8 | from .. import conf 9 | 10 | class Mac(object): 11 | access = None 12 | secret = None 13 | def __init__(self, access=None, secret=None): 14 | if access is None and secret is None: 15 | access, secret = conf.ACCESS_KEY, conf.SECRET_KEY 16 | self.access, self.secret = access, secret 17 | 18 | def __sign(self, data): 19 | hashed = hmac.new(self.secret, data, sha1) 20 | return urlsafe_b64encode(hashed.digest()) 21 | 22 | def sign(self, data): 23 | return '%s:%s' % (self.access, self.__sign(data)) 24 | 25 | def sign_with_data(self, b): 26 | data = urlsafe_b64encode(b) 27 | return '%s:%s:%s' % (self.access, self.__sign(data), data) 28 | 29 | def sign_request(self, path, body, content_type): 30 | parsedurl = urlparse(path) 31 | p_query = parsedurl.query 32 | p_path = parsedurl.path 33 | data = p_path 34 | if p_query != "": 35 | data = ''.join([data, '?', p_query]) 36 | data = ''.join([data, "\n"]) 37 | 38 | if body: 39 | incBody = [ 40 | "application/x-www-form-urlencoded", 41 | ] 42 | if content_type in incBody: 43 | data += body 44 | 45 | return '%s:%s' % (self.access, self.__sign(data)) 46 | 47 | 48 | class Client(rpc.Client): 49 | def __init__(self, host, mac=None): 50 | if mac is None: 51 | mac = Mac() 52 | super(Client, self).__init__(host) 53 | self.mac = mac 54 | 55 | def round_tripper(self, method, path, body): 56 | token = self.mac.sign_request(path, body, self._header.get("Content-Type")) 57 | self.set_header("Authorization", "QBox %s" % token) 58 | return super(Client, self).round_tripper(method, path, body) 59 | -------------------------------------------------------------------------------- /qiniu/auth/up.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .. import conf 3 | from .. import rpc 4 | 5 | 6 | class Client(rpc.Client): 7 | up_token = None 8 | 9 | def __init__(self, up_token, host=None): 10 | if host is None: 11 | host = conf.UP_HOST 12 | self.up_token = up_token 13 | super(Client, self).__init__(host) 14 | 15 | def round_tripper(self, method, path, body): 16 | self.set_header("Authorization", "UpToken %s" % self.up_token) 17 | return super(Client, self).round_tripper(method, path, body) 18 | -------------------------------------------------------------------------------- /qiniu/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ACCESS_KEY = "" 4 | SECRET_KEY = "" 5 | 6 | RS_HOST = "rs.qbox.me" 7 | RSF_HOST = "rsf.qbox.me" 8 | UP_HOST = "up.qiniu.com" 9 | 10 | from . import __version__ 11 | USER_AGENT = "qiniu python-sdk v%s" % __version__ 12 | -------------------------------------------------------------------------------- /qiniu/fop.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import json 3 | 4 | class Exif(object): 5 | def make_request(self, url): 6 | return '%s?exif' % url 7 | 8 | 9 | class ImageView(object): 10 | mode = 1 # 1或2 11 | width = None # width 默认为0,表示不限定宽度 12 | height = None 13 | quality = None # 图片质量, 1-100 14 | format = None # 输出格式, jpg, gif, png, tif 等图片格式 15 | 16 | def make_request(self, url): 17 | target = [] 18 | target.append('%s' % self.mode) 19 | 20 | if self.width is not None: 21 | target.append("w/%s" % self.width) 22 | 23 | if self.height is not None: 24 | target.append("h/%s" % self.height) 25 | 26 | if self.quality is not None: 27 | target.append("q/%s" % self.quality) 28 | 29 | if self.format is not None: 30 | target.append("format/%s" % self.format) 31 | 32 | return "%s?imageView/%s" % (url, '/'.join(target)) 33 | 34 | 35 | class ImageInfo(object): 36 | def make_request(self, url): 37 | return '%s?imageInfo' % url 38 | -------------------------------------------------------------------------------- /qiniu/httplib_chunk.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modified from standard httplib 3 | 4 | 1. HTTPConnection can send trunked data. 5 | 2. Remove httplib's automatic Content-Length insertion when data is a file-like object. 6 | """ 7 | 8 | # -*- coding: utf-8 -*- 9 | 10 | import httplib 11 | from httplib import _CS_REQ_STARTED, _CS_REQ_SENT 12 | import string 13 | import os 14 | from array import array 15 | 16 | class HTTPConnection(httplib.HTTPConnection): 17 | 18 | def send(self, data, is_chunked=False): 19 | """Send `data' to the server.""" 20 | if self.sock is None: 21 | if self.auto_open: 22 | self.connect() 23 | else: 24 | raise NotConnected() 25 | 26 | if self.debuglevel > 0: 27 | print "send:", repr(data) 28 | blocksize = 8192 29 | if hasattr(data,'read') and not isinstance(data, array): 30 | if self.debuglevel > 0: print "sendIng a read()able" 31 | datablock = data.read(blocksize) 32 | while datablock: 33 | if self.debuglevel > 0: 34 | print 'chunked:', is_chunked 35 | if is_chunked: 36 | if self.debuglevel > 0: print 'send: with trunked data' 37 | lenstr = string.upper(hex(len(datablock))[2:]) 38 | self.sock.sendall('%s\r\n%s\r\n' % (lenstr, datablock)) 39 | else: 40 | self.sock.sendall(datablock) 41 | datablock = data.read(blocksize) 42 | if is_chunked: 43 | self.sock.sendall('0\r\n\r\n') 44 | else: 45 | self.sock.sendall(data) 46 | 47 | 48 | def _set_content_length(self, body): 49 | # Set the content-length based on the body. 50 | thelen = None 51 | try: 52 | thelen = str(len(body)) 53 | except (TypeError, AttributeError), te: 54 | # Don't send a length if this failed 55 | if self.debuglevel > 0: print "Cannot stat!!" 56 | 57 | if thelen is not None: 58 | self.putheader('Content-Length', thelen) 59 | return True 60 | return False 61 | 62 | 63 | def _send_request(self, method, url, body, headers): 64 | # Honor explicitly requested Host: and Accept-Encoding: headers. 65 | header_names = dict.fromkeys([k.lower() for k in headers]) 66 | skips = {} 67 | if 'host' in header_names: 68 | skips['skip_host'] = 1 69 | if 'accept-encoding' in header_names: 70 | skips['skip_accept_encoding'] = 1 71 | 72 | self.putrequest(method, url, **skips) 73 | 74 | is_chunked = False 75 | if body and header_names.get('Transfer-Encoding') == 'chunked': 76 | is_chunked = True 77 | elif body and ('content-length' not in header_names): 78 | is_chunked = not self._set_content_length(body) 79 | if is_chunked: 80 | self.putheader('Transfer-Encoding', 'chunked') 81 | for hdr, value in headers.iteritems(): 82 | self.putheader(hdr, value) 83 | 84 | self.endheaders(body, is_chunked=is_chunked) 85 | 86 | 87 | def endheaders(self, message_body=None, is_chunked=False): 88 | """Indicate that the last header line has been sent to the server. 89 | 90 | This method sends the request to the server. The optional 91 | message_body argument can be used to pass a message body 92 | associated with the request. The message body will be sent in 93 | the same packet as the message headers if it is string, otherwise it is 94 | sent as a separate packet. 95 | """ 96 | if self.__state == _CS_REQ_STARTED: 97 | self.__state = _CS_REQ_SENT 98 | else: 99 | raise CannotSendHeader() 100 | self._send_output(message_body, is_chunked=is_chunked) 101 | 102 | 103 | def _send_output(self, message_body=None, is_chunked=False): 104 | """Send the currently buffered request and clear the buffer. 105 | 106 | Appends an extra \\r\\n to the buffer. 107 | A message_body may be specified, to be appended to the request. 108 | """ 109 | self._buffer.extend(("", "")) 110 | msg = "\r\n".join(self._buffer) 111 | del self._buffer[:] 112 | # If msg and message_body are sent in a single send() call, 113 | # it will avoid performance problems caused by the interaction 114 | # between delayed ack and the Nagle algorithm. 115 | if isinstance(message_body, str): 116 | msg += message_body 117 | message_body = None 118 | self.send(msg) 119 | if message_body is not None: 120 | #message_body was not a string (i.e. it is a file) and 121 | #we must run the risk of Nagle 122 | self.send(message_body, is_chunked=is_chunked) 123 | 124 | -------------------------------------------------------------------------------- /qiniu/io.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from base64 import urlsafe_b64encode 3 | import rpc 4 | import conf 5 | import random 6 | import string 7 | try: 8 | import zlib as binascii 9 | except ImportError: 10 | import binascii 11 | 12 | 13 | # @gist PutExtra 14 | class PutExtra(object): 15 | params = {} 16 | mime_type = 'application/octet-stream' 17 | crc32 = "" 18 | check_crc = 0 19 | # @endgist 20 | 21 | 22 | def put(uptoken, key, data, extra=None): 23 | """ put your data to Qiniu 24 | 25 | If key is None, the server will generate one. 26 | data may be str or read()able object. 27 | """ 28 | fields = { 29 | } 30 | 31 | if not extra: 32 | extra = PutExtra() 33 | 34 | if extra.params: 35 | for k in extra.params: 36 | fields[k] = str(extra.params[k]) 37 | 38 | if extra.check_crc: 39 | fields["crc32"] = str(extra.crc32) 40 | 41 | if key is not None: 42 | fields['key'] = key 43 | 44 | fields["token"] = uptoken 45 | 46 | fname = key 47 | if fname is None: 48 | fname = _random_str(9) 49 | elif fname is '': 50 | fname = 'index.html' 51 | files = [ 52 | {'filename': fname, 'data': data, 'mime_type': extra.mime_type}, 53 | ] 54 | return rpc.Client(conf.UP_HOST).call_with_multipart("/", fields, files) 55 | 56 | 57 | def put_file(uptoken, key, localfile, extra=None): 58 | """ put a file to Qiniu 59 | 60 | If key is None, the server will generate one. 61 | """ 62 | if extra is not None and extra.check_crc == 1: 63 | extra.crc32 = _get_file_crc32(localfile) 64 | with open(localfile, 'rb') as f: 65 | return put(uptoken, key, f, extra) 66 | 67 | 68 | _BLOCK_SIZE = 1024 * 1024 * 4 69 | 70 | def _get_file_crc32(filepath): 71 | with open(filepath, 'rb') as f: 72 | block = f.read(_BLOCK_SIZE) 73 | crc = 0 74 | while len(block) != 0: 75 | crc = binascii.crc32(block, crc) & 0xFFFFFFFF 76 | block = f.read(_BLOCK_SIZE) 77 | return crc 78 | 79 | 80 | def _random_str(length): 81 | lib = string.ascii_lowercase 82 | return ''.join([random.choice(lib) for i in range(0, length)]) 83 | -------------------------------------------------------------------------------- /qiniu/resumable_io.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | try: 4 | import zlib as binascii 5 | except ImportError: 6 | import binascii 7 | from base64 import urlsafe_b64encode 8 | 9 | import auth.up 10 | import conf 11 | 12 | _workers = 1 13 | _task_queue_size = _workers * 4 14 | _chunk_size = 256 * 1024 15 | _try_times = 3 16 | _block_size = 4 * 1024 * 1024 17 | 18 | class Error(Exception): 19 | value = None 20 | def __init__(self, value): 21 | self.value = value 22 | def __str__(self): 23 | return self.value 24 | 25 | err_invalid_put_progress = Error("invalid put progress") 26 | err_put_failed = Error("resumable put failed") 27 | err_unmatched_checksum = Error("unmatched checksum") 28 | 29 | def setup(chunk_size=0, try_times=0): 30 | """ 31 | * chunk_size => 默认的Chunk大小,不设定则为256k 32 | * try_times => 默认的尝试次数,不设定则为3 33 | """ 34 | global _chunk_size, _try_times 35 | 36 | if chunk_size == 0: 37 | chunk_size = 1 << 18 38 | 39 | if try_times == 0: 40 | try_times = 3 41 | 42 | _chunk_size, _try_times = chunk_size, try_times 43 | 44 | # ---------------------------------------------------------- 45 | def gen_crc32(data): 46 | return binascii.crc32(data) & 0xffffffff 47 | 48 | class PutExtra(object): 49 | callback_params = None # 当 uptoken 指定了 CallbackUrl,则 CallbackParams 必须非空 50 | bucket = None # 当前是必选项,但未来会去掉 51 | custom_meta = None # 可选。用户自定义 Meta,不能超过 256 字节 52 | mimetype = None # 可选。在 uptoken 没有指定 DetectMime 时,用户客户端可自己指定 MimeType 53 | chunk_size = None # 可选。每次上传的Chunk大小 54 | try_times = None # 可选。尝试次数 55 | progresses = None # 可选。上传进度 56 | notify = lambda self, idx, size, ret: None # 可选。进度提示 57 | notify_err = lambda self, idx, size, err: None 58 | 59 | def __init__(self, bucket): 60 | self.bucket = bucket 61 | 62 | def put_file(uptoken, key, localfile, extra): 63 | """ 上传文件 """ 64 | f = open(localfile, "rb") 65 | statinfo = os.stat(localfile) 66 | ret = put(uptoken, key, f, statinfo.st_size, extra) 67 | f.close() 68 | return ret 69 | 70 | def put(uptoken, key, f, fsize, extra): 71 | """ 上传二进制流, 通过将data "切片" 分段上传 """ 72 | if not isinstance(extra, PutExtra): 73 | print("extra must the instance of PutExtra") 74 | return 75 | 76 | block_cnt = block_count(fsize) 77 | if extra.progresses is None: 78 | extra.progresses = [None for i in xrange(0, block_cnt)] 79 | else: 80 | if not len(extra.progresses) == block_cnt: 81 | return None, err_invalid_put_progress 82 | 83 | if extra.try_times is None: 84 | extra.try_times = _try_times 85 | 86 | if extra.chunk_size is None: 87 | extra.chunk_size = _chunk_size 88 | 89 | client = auth.up.Client(uptoken) 90 | for i in xrange(0, block_cnt): 91 | try_time = extra.try_times 92 | read_length = _block_size 93 | if (i+1)*_block_size > fsize: 94 | read_length = fsize - i*_block_size 95 | data_slice = f.read(read_length) 96 | while True: 97 | err = resumable_block_put(client, data_slice, i, extra) 98 | if err is None: 99 | break 100 | 101 | try_time -= 1 102 | if try_time <= 0: 103 | return None, err_put_failed 104 | print err, ".. retry" 105 | 106 | return mkfile(client, key, fsize, extra) 107 | 108 | # ---------------------------------------------------------- 109 | 110 | def resumable_block_put(client, block, index, extra): 111 | block_size = len(block) 112 | 113 | if extra.progresses[index] is None or "ctx" not in extra.progresses[index]: 114 | end_pos = extra.chunk_size-1 115 | if block_size < extra.chunk_size: 116 | end_pos = block_size-1 117 | chunk = block[: end_pos] 118 | crc32 = gen_crc32(chunk) 119 | chunk = bytearray(chunk) 120 | extra.progresses[index], err = mkblock(client, block_size, chunk) 121 | if not extra.progresses[index]["crc32"] == crc32: 122 | return err_unmatched_checksum 123 | if err is not None: 124 | extra.notify_err(index, end_pos + 1, err) 125 | return err 126 | extra.notify(index, end_pos + 1, extra.progresses[index]) 127 | 128 | while extra.progresses[index]["offset"] < block_size: 129 | offset = extra.progresses[index]["offset"] 130 | chunk = block[offset: offset+extra.chunk_size-1] 131 | crc32 = gen_crc32(chunk) 132 | chunk = bytearray(chunk) 133 | extra.progresses[index], err = putblock(client, extra.progresses[index], chunk) 134 | if not extra.progresses[index]["crc32"] == crc32: 135 | return err_unmatched_checksum 136 | if err is not None: 137 | extra.notify_err(index, len(chunk), err) 138 | return err 139 | extra.notify(index, len(chunk), extra.progresses[index]) 140 | 141 | def block_count(size): 142 | global _block_size 143 | return size / _block_size + 1 144 | 145 | def mkblock(client, block_size, first_chunk): 146 | url = "http://%s/mkblk/%s" % (conf.UP_HOST, block_size) 147 | content_type = "application/octet-stream" 148 | return client.call_with(url, first_chunk, content_type, len(first_chunk)) 149 | 150 | def putblock(client, block_ret, chunk): 151 | url = "%s/bput/%s/%s" % (block_ret["host"], block_ret["ctx"], block_ret["offset"]) 152 | content_type = "application/octet-stream" 153 | return client.call_with(url, chunk, content_type, len(chunk)) 154 | 155 | def mkfile(client, key, fsize, extra): 156 | encoded_entry = urlsafe_b64encode("%s:%s" % (extra.bucket, key)) 157 | url = ["http://%s/rs-mkfile/%s/fsize/%s" % (conf.UP_HOST, encoded_entry, fsize)] 158 | 159 | if extra.mimetype: 160 | url.append("mimeType/%s" % urlsafe_b64encode(extra.mimetype)) 161 | 162 | if extra.custom_meta: 163 | url.append("meta/%s" % urlsafe_b64encode(extra.custom_meta)) 164 | 165 | if extra.callback_params: 166 | url.append("params/%s" % urlsafe_b64encode(extra.callback_params)) 167 | 168 | url = "/".join(url) 169 | body = ",".join([i["ctx"] for i in extra.progresses]) 170 | return client.call_with(url, body, "text/plain", len(body)) 171 | -------------------------------------------------------------------------------- /qiniu/rpc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import httplib_chunk as httplib 3 | import json 4 | import cStringIO 5 | import conf 6 | 7 | 8 | class Client(object): 9 | _conn = None 10 | _header = None 11 | 12 | def __init__(self, host): 13 | self._conn = httplib.HTTPConnection(host) 14 | self._header = {} 15 | 16 | def round_tripper(self, method, path, body): 17 | self._conn.request(method, path, body, self._header) 18 | resp = self._conn.getresponse() 19 | return resp 20 | 21 | def call(self, path): 22 | return self.call_with(path, None) 23 | 24 | def call_with(self, path, body, content_type=None, content_length=None): 25 | ret = None 26 | 27 | self.set_header("User-Agent", conf.USER_AGENT) 28 | if content_type is not None: 29 | self.set_header("Content-Type", content_type) 30 | 31 | if content_length is not None: 32 | self.set_header("Content-Length", content_length) 33 | 34 | resp = self.round_tripper("POST", path, body) 35 | try: 36 | ret = resp.read() 37 | ret = json.loads(ret) 38 | except IOError, e: 39 | return None, e 40 | except ValueError: 41 | pass 42 | 43 | if resp.status / 100 != 2: 44 | err_msg = ret if "error" not in ret else ret["error"] 45 | detail = resp.getheader("x-log", None) 46 | if detail is not None: 47 | err_msg += ", detail:%s" % detail 48 | 49 | return None, err_msg 50 | 51 | return ret, None 52 | 53 | def call_with_multipart(self, path, fields=None, files=None): 54 | """ 55 | * fields => {key} 56 | * files => [{filename, data, content_type}] 57 | """ 58 | content_type, mr = self.encode_multipart_formdata(fields, files) 59 | return self.call_with(path, mr, content_type, mr.length()) 60 | 61 | def call_with_form(self, path, ops): 62 | """ 63 | * ops => {"key": value/list()} 64 | """ 65 | 66 | body = [] 67 | for i in ops: 68 | if isinstance(ops[i], (list, tuple)): 69 | data = ('&%s=' % i).join(ops[i]) 70 | else: 71 | data = ops[i] 72 | 73 | body.append('%s=%s' % (i, data)) 74 | body = '&'.join(body) 75 | 76 | content_type = "application/x-www-form-urlencoded" 77 | return self.call_with(path, body, content_type, len(body)) 78 | 79 | def set_header(self, field, value): 80 | self._header[field] = value 81 | 82 | def set_headers(self, headers): 83 | self._header.update(headers) 84 | 85 | def encode_multipart_formdata(self, fields, files): 86 | """ 87 | * fields => {key} 88 | * files => [{filename, data, content_type}] 89 | * return content_type, content_length, body 90 | """ 91 | if files is None: 92 | files = [] 93 | if fields is None: 94 | fields = {} 95 | 96 | readers = [] 97 | BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' 98 | CRLF = '\r\n' 99 | L1 = [] 100 | for key in fields: 101 | L1.append('--' + BOUNDARY) 102 | L1.append('Content-Disposition: form-data; name="%s"' % key) 103 | L1.append('') 104 | L1.append(fields[key]) 105 | b1 = CRLF.join(L1) 106 | readers.append(b1) 107 | 108 | for file_info in files: 109 | L = [] 110 | L.append('') 111 | L.append('--' + BOUNDARY) 112 | disposition = "Content-Disposition: form-data;" 113 | filename = _qiniu_escape(file_info.get('filename')) 114 | L.append('%s name="file"; filename="%s"' % (disposition, filename)) 115 | L.append('Content-Type: %s' % file_info.get('content_type', 'application/octet-stream')) 116 | L.append('') 117 | L.append('') 118 | b2 = CRLF.join(L) 119 | readers.append(b2) 120 | 121 | data = file_info.get('data') 122 | readers.append(data) 123 | 124 | L3 = ['', '--' + BOUNDARY + '--', ''] 125 | b3 = CRLF.join(L3) 126 | readers.append(b3) 127 | 128 | content_type = 'multipart/form-data; boundary=%s' % BOUNDARY 129 | return content_type, MultiReader(readers) 130 | 131 | def _qiniu_escape(s): 132 | edits = [('\\', '\\\\'), ('\"', '\\\"')] 133 | for (search, replace) in edits: 134 | s = s.replace(search, replace) 135 | return s 136 | 137 | 138 | class MultiReader(object): 139 | """ class MultiReader([readers...]) 140 | 141 | MultiReader returns a read()able object that's the logical concatenation of 142 | the provided input readers. They're read sequentially. 143 | """ 144 | 145 | def __init__(self, readers): 146 | self.readers = [] 147 | self.content_length = 0 148 | self.valid_content_length = True 149 | for r in readers: 150 | if hasattr(r, 'read'): 151 | if self.valid_content_length: 152 | length = self._get_content_length(r) 153 | if length is not None: 154 | self.content_length += length 155 | else: 156 | self.valid_content_length = False 157 | else: 158 | buf = r 159 | if not isinstance(buf, basestring): 160 | buf = str(buf) 161 | buf = encode_unicode(buf) 162 | r = cStringIO.StringIO(buf) 163 | self.content_length += len(buf) 164 | self.readers.append(r) 165 | 166 | 167 | # don't name it __len__, because the length of MultiReader is not alway valid. 168 | def length(self): 169 | return self.content_length if self.valid_content_length else None 170 | 171 | 172 | def _get_content_length(self, reader): 173 | data_len = None 174 | if hasattr(reader, 'seek') and hasattr(reader, 'tell'): 175 | try: 176 | reader.seek(0, 2) 177 | data_len= reader.tell() 178 | reader.seek(0, 0) 179 | except OSError: 180 | # Don't send a length if this failed 181 | data_len = None 182 | return data_len 183 | 184 | def read(self, n=-1): 185 | if n is None or n == -1: 186 | return ''.join([encode_unicode(r.read()) for r in self.readers]) 187 | else: 188 | L = [] 189 | while len(self.readers) > 0 and n > 0: 190 | b = self.readers[0].read(n) 191 | if len(b) == 0: 192 | self.readers = self.readers[1:] 193 | else: 194 | L.append(encode_unicode(b)) 195 | n -= len(b) 196 | return ''.join(L) 197 | 198 | 199 | def encode_unicode(u): 200 | if isinstance(u, unicode): 201 | u = u.encode('utf8') 202 | return u 203 | -------------------------------------------------------------------------------- /qiniu/rs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __all__ = [ 4 | "Client", "EntryPath", "EntryPathPair", "uri_stat", "uri_delete", "uri_move", "uri_copy", 5 | "PutPolicy", "GetPolicy", "make_base_url", 6 | ] 7 | 8 | from .rs import * 9 | from .rs_token import * 10 | -------------------------------------------------------------------------------- /qiniu/rs/rs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from base64 import urlsafe_b64encode 3 | 4 | from ..auth import digest 5 | from .. import conf 6 | 7 | class Client(object): 8 | conn = None 9 | def __init__(self, mac=None): 10 | if mac is None: 11 | mac = digest.Mac() 12 | self.conn = digest.Client(host=conf.RS_HOST, mac=mac) 13 | 14 | def stat(self, bucket, key): 15 | return self.conn.call(uri_stat(bucket, key)) 16 | 17 | def delete(self, bucket, key): 18 | return self.conn.call(uri_delete(bucket, key)) 19 | 20 | def move(self, bucket_src, key_src, bucket_dest, key_dest): 21 | return self.conn.call(uri_move(bucket_src, key_src, bucket_dest, key_dest)) 22 | 23 | def copy(self, bucket_src, key_src, bucket_dest, key_dest): 24 | return self.conn.call(uri_copy(bucket_src, key_src, bucket_dest, key_dest)) 25 | 26 | def batch(self, ops): 27 | return self.conn.call_with_form("/batch", dict(op=ops)) 28 | 29 | def batch_stat(self, entries): 30 | ops = [] 31 | for entry in entries: 32 | ops.append(uri_stat(entry.bucket, entry.key)) 33 | return self.batch(ops) 34 | 35 | def batch_delete(self, entries): 36 | ops = [] 37 | for entry in entries: 38 | ops.append(uri_delete(entry.bucket, entry.key)) 39 | return self.batch(ops) 40 | 41 | def batch_move(self, entries): 42 | ops = [] 43 | for entry in entries: 44 | ops.append(uri_move(entry.src.bucket, entry.src.key, 45 | entry.dest.bucket, entry.dest.key)) 46 | return self.batch(ops) 47 | 48 | def batch_copy(self, entries): 49 | ops = [] 50 | for entry in entries: 51 | ops.append(uri_copy(entry.src.bucket, entry.src.key, 52 | entry.dest.bucket, entry.dest.key)) 53 | return self.batch(ops) 54 | 55 | class EntryPath(object): 56 | bucket = None 57 | key = None 58 | def __init__(self, bucket, key): 59 | self.bucket = bucket 60 | self.key = key 61 | 62 | class EntryPathPair: 63 | src = None 64 | dest = None 65 | def __init__(self, src, dest): 66 | self.src = src 67 | self.dest = dest 68 | 69 | def uri_stat(bucket, key): 70 | return "/stat/%s" % urlsafe_b64encode("%s:%s" % (bucket, key)) 71 | 72 | def uri_delete(bucket, key): 73 | return "/delete/%s" % urlsafe_b64encode("%s:%s" % (bucket, key)) 74 | 75 | def uri_move(bucket_src, key_src, bucket_dest, key_dest): 76 | src = urlsafe_b64encode("%s:%s" % (bucket_src, key_src)) 77 | dest = urlsafe_b64encode("%s:%s" % (bucket_dest, key_dest)) 78 | return "/move/%s/%s" % (src, dest) 79 | 80 | def uri_copy(bucket_src, key_src, bucket_dest, key_dest): 81 | src = urlsafe_b64encode("%s:%s" % (bucket_src, key_src)) 82 | dest = urlsafe_b64encode("%s:%s" % (bucket_dest, key_dest)) 83 | return "/copy/%s/%s" % (src, dest) 84 | -------------------------------------------------------------------------------- /qiniu/rs/rs_token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import time 4 | import urllib 5 | 6 | from ..auth import digest 7 | from ..import rpc 8 | 9 | # @gist PutPolicy 10 | class PutPolicy(object): 11 | scope = None # 可以是 bucketName 或者 bucketName:key 12 | expires = 3600 # 默认是 3600 秒 13 | callbackUrl = None 14 | callbackBody = None 15 | returnUrl = None 16 | returnBody = None 17 | endUser = None 18 | asyncOps = None 19 | 20 | def __init__(self, scope): 21 | self.scope = scope 22 | # @endgist 23 | 24 | def token(self, mac=None): 25 | if mac is None: 26 | mac = digest.Mac() 27 | token = dict( 28 | scope = self.scope, 29 | deadline = int(time.time()) + self.expires, 30 | ) 31 | 32 | if self.callbackUrl is not None: 33 | token["callbackUrl"] = self.callbackUrl 34 | 35 | if self.callbackBody is not None: 36 | token["callbackBody"] = self.callbackBody 37 | 38 | if self.returnUrl is not None: 39 | token["returnUrl"] = self.returnUrl 40 | 41 | if self.returnBody is not None: 42 | token["returnBody"] = self.returnBody 43 | 44 | if self.endUser is not None: 45 | token["endUser"] = self.endUser 46 | 47 | if self.asyncOps is not None: 48 | token["asyncOps"] = self.asyncOps 49 | 50 | b = json.dumps(token, separators=(',',':')) 51 | return mac.sign_with_data(b) 52 | 53 | class GetPolicy(object): 54 | expires = 3600 55 | def __init__(self): 56 | pass 57 | 58 | def make_request(self, base_url, mac=None): 59 | ''' 60 | * return private_url 61 | ''' 62 | if mac is None: 63 | mac = digest.Mac() 64 | 65 | deadline = int(time.time()) + self.expires 66 | if '?' in base_url: 67 | base_url += '&' 68 | else: 69 | base_url += '?' 70 | base_url = '%se=%s' % (base_url, str(deadline)) 71 | 72 | token = mac.sign(base_url) 73 | return '%s&token=%s' % (base_url, token) 74 | 75 | 76 | def make_base_url(domain, key): 77 | ''' 78 | * domain => str 79 | * key => str 80 | * return base_url 81 | ''' 82 | key = rpc.encode_unicode(key) 83 | return 'http://%s/%s' % (domain, urllib.quote(key)) 84 | -------------------------------------------------------------------------------- /qiniu/rs/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import urllib 4 | 5 | import qiniu.io 6 | import qiniu.rs 7 | import qiniu.conf 8 | 9 | pic = "http://cheneya.qiniudn.com/hello_jpg" 10 | key = 'QINIU_UNIT_TEST_PIC' 11 | 12 | def setUp(): 13 | qiniu.conf.ACCESS_KEY = os.getenv("QINIU_ACCESS_KEY") 14 | qiniu.conf.SECRET_KEY = os.getenv("QINIU_SECRET_KEY") 15 | bucket_name = os.getenv("QINIU_TEST_BUCKET") 16 | 17 | policy = qiniu.rs.PutPolicy(bucket_name) 18 | uptoken = policy.token() 19 | 20 | f = urllib.urlopen(pic) 21 | _, err = qiniu.io.put(uptoken, key, f) 22 | f.close() 23 | if err is None or err.startswith('file exists'): 24 | print err 25 | assert err is None or err.startswith('file exists') 26 | -------------------------------------------------------------------------------- /qiniu/rs/test/rs_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import os 4 | import random 5 | import string 6 | 7 | from qiniu import rs 8 | from qiniu import conf 9 | 10 | def r(length): 11 | lib = string.ascii_uppercase 12 | return ''.join([random.choice(lib) for i in range(0, length)]) 13 | 14 | conf.ACCESS_KEY = os.getenv("QINIU_ACCESS_KEY") 15 | conf.SECRET_KEY = os.getenv("QINIU_SECRET_KEY") 16 | key = 'QINIU_UNIT_TEST_PIC' 17 | bucket_name = os.getenv("QINIU_TEST_BUCKET") 18 | noexist_key = 'QINIU_UNIT_TEST_NOEXIST' + r(30) 19 | key2 = "rs_demo_test_key_1_" + r(5) 20 | key3 = "rs_demo_test_key_2_" + r(5) 21 | key4 = "rs_demo_test_key_3_" + r(5) 22 | 23 | class TestRs(unittest.TestCase): 24 | def test_stat(self): 25 | r = rs.Client() 26 | ret, err = r.stat(bucket_name, key) 27 | assert err is None 28 | assert ret is not None 29 | 30 | # error 31 | _, err = r.stat(bucket_name, noexist_key) 32 | assert err is not None 33 | 34 | def test_delete_move_copy(self): 35 | r = rs.Client() 36 | r.delete(bucket_name, key2) 37 | r.delete(bucket_name, key3) 38 | 39 | ret, err = r.copy(bucket_name, key, bucket_name, key2) 40 | assert err is None, err 41 | 42 | ret, err = r.move(bucket_name, key2, bucket_name, key3) 43 | assert err is None, err 44 | 45 | ret, err = r.delete(bucket_name, key3) 46 | assert err is None, err 47 | 48 | # error 49 | _, err = r.delete(bucket_name, key2) 50 | assert err is not None 51 | 52 | _, err = r.delete(bucket_name, key3) 53 | assert err is not None 54 | 55 | def test_batch_stat(self): 56 | r = rs.Client() 57 | entries = [ 58 | rs.EntryPath(bucket_name, key), 59 | rs.EntryPath(bucket_name, key2), 60 | ] 61 | ret, err = r.batch_stat(entries) 62 | assert err is None 63 | self.assertEqual(ret[0]["code"], 200) 64 | self.assertEqual(ret[1]["code"], 612) 65 | 66 | def test_batch_delete_move_copy(self): 67 | r = rs.Client() 68 | e1 = rs.EntryPath(bucket_name, key) 69 | e2 = rs.EntryPath(bucket_name, key2) 70 | e3 = rs.EntryPath(bucket_name, key3) 71 | e4 = rs.EntryPath(bucket_name, key4) 72 | r.batch_delete([e2, e3, e4]) 73 | 74 | # copy 75 | entries = [ 76 | rs.EntryPathPair(e1, e2), 77 | rs.EntryPathPair(e1, e3), 78 | ] 79 | ret, err = r.batch_copy(entries) 80 | assert err is None 81 | self.assertEqual(ret[0]["code"], 200) 82 | self.assertEqual(ret[1]["code"], 200) 83 | 84 | ret, err = r.batch_move([rs.EntryPathPair(e2, e4)]) 85 | assert err is None 86 | self.assertEqual(ret[0]["code"], 200) 87 | 88 | ret, err = r.batch_delete([e3, e4]) 89 | assert err is None 90 | self.assertEqual(ret[0]["code"], 200) 91 | 92 | r.batch_delete([e2, e3, e4]) 93 | 94 | if __name__ == "__main__": 95 | unittest.main() 96 | -------------------------------------------------------------------------------- /qiniu/rs/test/rs_token_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import os 4 | import json 5 | from base64 import urlsafe_b64decode as decode 6 | from base64 import urlsafe_b64encode as encode 7 | from hashlib import sha1 8 | import hmac 9 | import urllib 10 | 11 | from qiniu import conf 12 | from qiniu import rpc 13 | from qiniu import rs 14 | 15 | conf.ACCESS_KEY = os.getenv("QINIU_ACCESS_KEY") 16 | conf.SECRET_KEY = os.getenv("QINIU_SECRET_KEY") 17 | bucket_name = os.getenv("QINIU_TEST_BUCKET") 18 | domain = os.getenv("QINIU_TEST_DOMAIN") 19 | key = 'QINIU_UNIT_TEST_PIC' 20 | 21 | class TestToken(unittest.TestCase): 22 | def test_put_policy(self): 23 | policy = rs.PutPolicy(bucket_name) 24 | policy.endUser = "hello!" 25 | tokens = policy.token().split(':') 26 | self.assertEqual(conf.ACCESS_KEY, tokens[0]) 27 | data = json.loads(decode(tokens[2])) 28 | self.assertEqual(data["scope"], bucket_name) 29 | self.assertEqual(data["endUser"], policy.endUser) 30 | 31 | new_hmac = encode(hmac.new(conf.SECRET_KEY, tokens[2], sha1).digest()) 32 | self.assertEqual(new_hmac, tokens[1]) 33 | 34 | def test_get_policy(self): 35 | base_url = rs.make_base_url(domain, key) 36 | policy = rs.GetPolicy() 37 | private_url = policy.make_request(base_url) 38 | 39 | f = urllib.urlopen(private_url) 40 | body = f.read() 41 | f.close() 42 | self.assertEqual(len(body)>100, True) 43 | 44 | 45 | class Test_make_base_url(unittest.TestCase): 46 | def test_unicode(self): 47 | url1 = rs.make_base_url('1.com', '你好') 48 | url2 = rs.make_base_url('1.com', u'你好') 49 | assert url1 == url2 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /qiniu/rsf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import auth.digest 3 | import conf 4 | import urllib 5 | 6 | EOF = 'EOF' 7 | 8 | 9 | class Client(object): 10 | conn = None 11 | def __init__(self, mac=None): 12 | if mac is None: 13 | mac = auth.digest.Mac() 14 | self.conn = auth.digest.Client(host=conf.RSF_HOST, mac=mac) 15 | 16 | def list_prefix(self, bucket, prefix=None, marker=None, limit=None): 17 | '''前缀查询: 18 | * bucket => str 19 | * prefix => str 20 | * marker => str 21 | * limit => int 22 | * return ret => {'items': items, 'marker': markerOut}, err => str 23 | 24 | 1. 首次请求 marker = None 25 | 2. 无论 err 值如何,均应该先看 ret.get('items') 是否有内容 26 | 3. 如果后续没有更多数据,err 返回 EOF,markerOut 返回 None(但不通过该特征来判断是否结束) 27 | ''' 28 | ops = { 29 | 'bucket': bucket, 30 | } 31 | if marker is not None: 32 | ops['marker'] = marker 33 | if limit is not None: 34 | ops['limit'] = limit 35 | if prefix is not None: 36 | ops['prefix'] = prefix 37 | url = '%s?%s' % ('/list', urllib.urlencode(ops)) 38 | ret, err = self.conn.call_with(url, body=None, content_type='application/x-www-form-urlencoded') 39 | if not ret.get('marker'): 40 | err = EOF 41 | return ret, err 42 | -------------------------------------------------------------------------------- /qiniu/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/qiniu/test/__init__.py -------------------------------------------------------------------------------- /qiniu/test/conf_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | from qiniu import conf 4 | 5 | class TestConfig(unittest.TestCase): 6 | def test_USER_AGENT(self): 7 | assert len(conf.USER_AGENT) >= len('qiniu python-sdk') 8 | 9 | if __name__ == '__main__': 10 | unittest.main() 11 | -------------------------------------------------------------------------------- /qiniu/test/fop_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import unittest 3 | import os 4 | from qiniu import fop 5 | 6 | pic = "http://cheneya.qiniudn.com/hello_jpg" 7 | 8 | class TestFop(unittest.TestCase): 9 | def test_exif(self): 10 | ie = fop.Exif() 11 | ret = ie.make_request(pic) 12 | self.assertEqual(ret, "%s?exif" % pic) 13 | 14 | def test_imageView(self): 15 | iv = fop.ImageView() 16 | iv.height = 100 17 | ret = iv.make_request(pic) 18 | self.assertEqual(ret, "%s?imageView/1/h/100" % pic) 19 | 20 | iv.quality = 20 21 | iv.format = "png" 22 | ret = iv.make_request(pic) 23 | self.assertEqual(ret, "%s?imageView/1/h/100/q/20/format/png" % pic) 24 | 25 | def test_imageInfo(self): 26 | ii = fop.ImageInfo() 27 | ret = ii.make_request(pic) 28 | self.assertEqual(ret, "%s?imageInfo" % pic) 29 | 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /qiniu/test/io_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import unittest 4 | import string 5 | import random 6 | import urllib 7 | try: 8 | import zlib as binascii 9 | except ImportError: 10 | import binascii 11 | import cStringIO 12 | 13 | from qiniu import conf 14 | from qiniu import rs 15 | from qiniu import io 16 | 17 | conf.ACCESS_KEY = os.getenv("QINIU_ACCESS_KEY") 18 | conf.SECRET_KEY = os.getenv("QINIU_SECRET_KEY") 19 | bucket_name = os.getenv("QINIU_TEST_BUCKET") 20 | 21 | policy = rs.PutPolicy(bucket_name) 22 | extra = io.PutExtra() 23 | extra.mime_type = "text/plain" 24 | extra.params = {'x:a':'a'} 25 | 26 | def r(length): 27 | lib = string.ascii_uppercase 28 | return ''.join([random.choice(lib) for i in range(0, length)]) 29 | 30 | class TestUp(unittest.TestCase): 31 | def test(self): 32 | def test_put(): 33 | key = "test_%s" % r(9) 34 | params = "op=3" 35 | data = "hello bubby!" 36 | extra.check_crc = 2 37 | extra.crc32 = binascii.crc32(data) & 0xFFFFFFFF 38 | ret, err = io.put(policy.token(), key, data, extra) 39 | assert err is None 40 | assert ret['key'] == key 41 | 42 | def test_put_same_crc(): 43 | key = "test_%s" % r(9) 44 | data = "hello bubby!" 45 | extra.check_crc = 2 46 | ret, err = io.put(policy.token(), key, data, extra) 47 | assert err is None 48 | assert ret['key'] == key 49 | 50 | def test_put_no_key(): 51 | data = r(100) 52 | extra.check_crc = 0 53 | ret, err = io.put(policy.token(), key=None, data=data, extra=extra) 54 | assert err is None 55 | assert ret['hash'] == ret['key'] 56 | 57 | def test_put_quote_key(): 58 | data = r(100) 59 | key = 'a\\b\\c"你好' + r(9) 60 | ret, err = io.put(policy.token(), key, data) 61 | print err 62 | assert err is None 63 | assert ret['key'].encode('utf8') == key 64 | 65 | data = r(100) 66 | key = u'a\\b\\c"你好' + r(9) 67 | ret, err = io.put(policy.token(), key, data) 68 | assert err is None 69 | assert ret['key'] == key 70 | 71 | def test_put_unicode1(): 72 | key = "test_%s" % r(9) + '你好' 73 | data = key 74 | ret, err = io.put(policy.token(), key, data, extra) 75 | assert err is None 76 | assert ret[u'key'].endswith(u'你好') 77 | 78 | def test_put_unicode2(): 79 | key = "test_%s" % r(9) + '你好' 80 | data = key 81 | data = data.decode('utf8') 82 | ret, err = io.put(policy.token(), key, data) 83 | assert err is None 84 | assert ret[u'key'].endswith(u'你好') 85 | 86 | def test_put_unicode3(): 87 | key = "test_%s" % r(9) + '你好' 88 | data = key 89 | key = key.decode('utf8') 90 | ret, err = io.put(policy.token(), key, data) 91 | assert err is None 92 | assert ret[u'key'].endswith(u'你好') 93 | 94 | def test_put_unicode4(): 95 | key = "test_%s" % r(9) + '你好' 96 | data = key 97 | key = key.decode('utf8') 98 | data = data.decode('utf8') 99 | ret, err = io.put(policy.token(), key, data) 100 | assert err is None 101 | assert ret[u'key'].endswith(u'你好') 102 | 103 | def test_put_StringIO(): 104 | key = "test_%s" % r(9) 105 | data = cStringIO.StringIO('hello buddy!') 106 | ret, err = io.put(policy.token(), key, data) 107 | assert err is None 108 | assert ret['key'] == key 109 | 110 | def test_put_urlopen(): 111 | key = "test_%s" % r(9) 112 | data = urllib.urlopen('http://cheneya.qiniudn.com/hello_jpg') 113 | ret, err = io.put(policy.token(), key, data) 114 | assert err is None 115 | assert ret['key'] == key 116 | 117 | def test_put_no_length(): 118 | class test_reader(object): 119 | def __init__(self): 120 | self.data = 'abc' 121 | self.pos = 0 122 | def read(self, n=None): 123 | if n is None or n < 0: 124 | newpos = len(self.data) 125 | else: 126 | newpos = min(self.pos+n, len(self.data)) 127 | r = self.data[self.pos: newpos] 128 | self.pos = newpos 129 | return r 130 | key = "test_%s" % r(9) 131 | data = test_reader() 132 | 133 | extra.check_crc = 2 134 | extra.crc32 = binascii.crc32('abc') & 0xFFFFFFFF 135 | ret, err = io.put(policy.token(), key, data, extra) 136 | assert err is None 137 | assert ret['key'] == key 138 | 139 | test_put() 140 | test_put_same_crc() 141 | test_put_no_key() 142 | test_put_quote_key() 143 | test_put_unicode1() 144 | test_put_unicode2() 145 | test_put_unicode3() 146 | test_put_unicode4() 147 | test_put_StringIO() 148 | test_put_urlopen() 149 | test_put_no_length() 150 | 151 | def test_put_file(self): 152 | localfile = "%s" % __file__ 153 | key = "test_%s" % r(9) 154 | 155 | extra.check_crc = 1 156 | ret, err = io.put_file(policy.token(), key, localfile, extra) 157 | assert err is None 158 | assert ret['key'] == key 159 | 160 | def test_put_crc_fail(self): 161 | key = "test_%s" % r(9) 162 | data = "hello bubby!" 163 | extra.check_crc = 2 164 | extra.crc32 = "wrong crc32" 165 | ret, err = io.put(policy.token(), key, data, extra) 166 | assert err is not None 167 | 168 | 169 | class Test_get_file_crc32(unittest.TestCase): 170 | def test_get_file_crc32(self): 171 | file_path = '%s' % __file__ 172 | 173 | data = None 174 | with open(file_path, 'rb') as f: 175 | data = f.read() 176 | io._BLOCK_SIZE = 4 177 | assert binascii.crc32(data) & 0xFFFFFFFF == io._get_file_crc32(file_path) 178 | 179 | 180 | if __name__ == "__main__": 181 | unittest.main() 182 | -------------------------------------------------------------------------------- /qiniu/test/resumable_io_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import unittest 4 | import string 5 | import random 6 | import platform 7 | try: 8 | import zlib as binascii 9 | except ImportError: 10 | import binascii 11 | import urllib 12 | import tempfile 13 | import shutil 14 | 15 | from qiniu import conf 16 | from qiniu.auth import up 17 | from qiniu import resumable_io 18 | from qiniu import rs 19 | 20 | bucket = os.getenv("QINIU_TEST_BUCKET") 21 | conf.ACCESS_KEY = os.getenv("QINIU_ACCESS_KEY") 22 | conf.SECRET_KEY = os.getenv("QINIU_SECRET_KEY") 23 | 24 | 25 | def r(length): 26 | lib = string.ascii_uppercase 27 | return ''.join([random.choice(lib) for i in range(0, length)]) 28 | 29 | class TestBlock(unittest.TestCase): 30 | def test_block(self): 31 | policy = rs.PutPolicy(bucket) 32 | uptoken = policy.token() 33 | client = up.Client(uptoken) 34 | 35 | rets = [0, 0] 36 | data_slice_2 = "\nbye!" 37 | ret, err = resumable_io.mkblock(client, len(data_slice_2), data_slice_2) 38 | assert err is None, err 39 | self.assertEqual(ret["crc32"], binascii.crc32(data_slice_2)) 40 | 41 | extra = resumable_io.PutExtra(bucket) 42 | extra.mimetype = "text/plain" 43 | extra.progresses = [ret] 44 | lens = 0 45 | for i in xrange(0, len(extra.progresses)): 46 | lens += extra.progresses[i]["offset"] 47 | 48 | key = u"sdk_py_resumable_block_4_%s" % r(9) 49 | ret, err = resumable_io.mkfile(client, key, lens, extra) 50 | assert err is None, err 51 | self.assertEqual(ret["hash"], "FtCFo0mQugW98uaPYgr54Vb1QsO0", "hash not match") 52 | rs.Client().delete(bucket, key) 53 | 54 | def test_put(self): 55 | src = urllib.urlopen("http://cheneya.qiniudn.com/hello_jpg") 56 | ostype = platform.system() 57 | if ostype.lower().find("windows") != -1: 58 | tmpf = "".join([os.getcwd(), os.tmpnam()]) 59 | else: 60 | tmpf = os.tmpnam() 61 | dst = open(tmpf, 'wb') 62 | shutil.copyfileobj(src, dst) 63 | src.close() 64 | 65 | policy = rs.PutPolicy(bucket) 66 | extra = resumable_io.PutExtra(bucket) 67 | extra.bucket = bucket 68 | key = "sdk_py_resumable_block_5_%s" % r(9) 69 | localfile = dst.name 70 | ret, err = resumable_io.put_file(policy.token(), key, localfile, extra) 71 | dst.close() 72 | os.remove(tmpf) 73 | 74 | assert err is None, err 75 | self.assertEqual(ret["hash"], "FnyTMUqPNRTdk1Wou7oLqDHkBm_p", "hash not match") 76 | rs.Client().delete(bucket, key) 77 | 78 | 79 | if __name__ == "__main__": 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /qiniu/test/rpc_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import StringIO 3 | import unittest 4 | 5 | from qiniu import rpc 6 | from qiniu import conf 7 | 8 | def round_tripper(client, method, path, body): 9 | pass 10 | 11 | class ClsTestClient(rpc.Client): 12 | def round_tripper(self, method, path, body): 13 | round_tripper(self, method, path, body) 14 | return super(ClsTestClient, self).round_tripper(method, path, body) 15 | 16 | client = ClsTestClient(conf.RS_HOST) 17 | 18 | class TestClient(unittest.TestCase): 19 | def test_call(self): 20 | global round_tripper 21 | 22 | def tripper(client, method, path, body): 23 | self.assertEqual(path, "/hello") 24 | assert body is None 25 | 26 | round_tripper = tripper 27 | client.call("/hello") 28 | 29 | def test_call_with(self): 30 | global round_tripper 31 | def tripper(client, method, path, body): 32 | self.assertEqual(body, "body") 33 | 34 | round_tripper = tripper 35 | client.call_with("/hello", "body") 36 | 37 | def test_call_with_multipart(self): 38 | global round_tripper 39 | def tripper(client, method, path, body): 40 | target_type = "multipart/form-data" 41 | self.assertTrue(client._header["Content-Type"].startswith(target_type)) 42 | start_index = client._header["Content-Type"].find("boundary") 43 | boundary = client._header["Content-Type"][start_index + 9: ] 44 | dispostion = 'Content-Disposition: form-data; name="auth"' 45 | tpl = "--%s\r\n%s\r\n\r\n%s\r\n--%s--\r\n" % (boundary, dispostion, 46 | "auth_string", boundary) 47 | self.assertEqual(len(tpl), client._header["Content-Length"]) 48 | self.assertEqual(len(tpl), body.length()) 49 | 50 | round_tripper = tripper 51 | client.call_with_multipart("/hello", fields={"auth": "auth_string"}) 52 | 53 | def test_call_with_form(self): 54 | global round_tripper 55 | def tripper(client, method, path, body): 56 | self.assertEqual(body, "action=a&op=a&op=b") 57 | target_type = "application/x-www-form-urlencoded" 58 | self.assertEqual(client._header["Content-Type"], target_type) 59 | self.assertEqual(client._header["Content-Length"], len(body)) 60 | 61 | round_tripper = tripper 62 | client.call_with_form("/hello", dict(op=["a", "b"], action="a")) 63 | 64 | 65 | class TestMultiReader(unittest.TestCase): 66 | def test_multi_reader1(self): 67 | a = StringIO.StringIO('你好') 68 | b = StringIO.StringIO('abcdefg') 69 | c = StringIO.StringIO(u'悲剧') 70 | mr = rpc.MultiReader([a, b, c]) 71 | data = mr.read() 72 | assert data.index('悲剧') > data.index('abcdefg') 73 | 74 | def test_multi_reader2(self): 75 | a = StringIO.StringIO('你好') 76 | b = StringIO.StringIO('abcdefg') 77 | c = StringIO.StringIO(u'悲剧') 78 | mr = rpc.MultiReader([a, b, c]) 79 | data = mr.read(8) 80 | assert len(data) is 8 81 | 82 | 83 | def encode_multipart_formdata2(fields, files): 84 | if files is None: 85 | files = [] 86 | if fields is None: 87 | fields = [] 88 | 89 | BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' 90 | CRLF = '\r\n' 91 | L = [] 92 | for (key, value) in fields: 93 | L.append('--' + BOUNDARY) 94 | L.append('Content-Disposition: form-data; name="%s"' % key) 95 | L.append('') 96 | L.append(value) 97 | for (key, filename, value) in files: 98 | L.append('--' + BOUNDARY) 99 | disposition = "Content-Disposition: form-data;" 100 | L.append('%s name="%s"; filename="%s"' % (disposition, key, filename)) 101 | L.append('Content-Type: application/octet-stream') 102 | L.append('') 103 | L.append(value) 104 | L.append('--' + BOUNDARY + '--') 105 | L.append('') 106 | body = CRLF.join(L) 107 | content_type = 'multipart/form-data; boundary=%s' % BOUNDARY 108 | return content_type, body 109 | 110 | 111 | class TestEncodeMultipartFormdata(unittest.TestCase): 112 | def test_encode(self): 113 | fields = {'a': '1', 'b': '2'} 114 | files = [ 115 | { 116 | 'filename': 'key1', 117 | 'data': 'data1', 118 | 'mime_type': 'application/octet-stream', 119 | }, 120 | { 121 | 'filename': 'key2', 122 | 'data': 'data2', 123 | 'mime_type': 'application/octet-stream', 124 | } 125 | ] 126 | content_type, mr = rpc.Client('localhost').encode_multipart_formdata(fields, files) 127 | t, b = encode_multipart_formdata2( 128 | [('a', '1'), ('b', '2')], 129 | [('file', 'key1', 'data1'), ('file', 'key2', 'data2')] 130 | ) 131 | assert t == content_type 132 | assert len(b) == mr.length() 133 | 134 | def test_unicode(self): 135 | def test1(): 136 | files = [{'filename': '你好', 'data': '你好', 'mime_type': ''}] 137 | _, body = rpc.Client('localhost').encode_multipart_formdata(None, files) 138 | return len(body.read()) 139 | def test2(): 140 | files = [{'filename': u'你好', 'data': '你好', 'mime_type': ''}] 141 | _, body = rpc.Client('localhost').encode_multipart_formdata(None, files) 142 | return len(body.read()) 143 | def test3(): 144 | files = [{'filename': '你好', 'data': u'你好', 'mime_type': ''}] 145 | _, body = rpc.Client('localhost').encode_multipart_formdata(None, files) 146 | return len(body.read()) 147 | def test4(): 148 | files = [{'filename': u'你好', 'data': u'你好', 'mime_type': ''}] 149 | _, body = rpc.Client('localhost').encode_multipart_formdata(None, files) 150 | return len(body.read()) 151 | 152 | assert test1() == test2() 153 | assert test2() == test3() 154 | assert test3() == test4() 155 | 156 | 157 | if __name__ == "__main__": 158 | unittest.main() 159 | -------------------------------------------------------------------------------- /qiniu/test/rsf_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | from qiniu import rsf 4 | from qiniu import conf 5 | 6 | import os 7 | conf.ACCESS_KEY = os.getenv("QINIU_ACCESS_KEY") 8 | conf.SECRET_KEY = os.getenv("QINIU_SECRET_KEY") 9 | bucket_name = os.getenv("QINIU_TEST_BUCKET") 10 | 11 | class TestRsf(unittest.TestCase): 12 | def test_list_prefix(self): 13 | c = rsf.Client() 14 | ret, err = c.list_prefix(bucket_name, limit = 1) 15 | self.assertEqual(err is rsf.EOF or err is None, True) 16 | assert len(ret.get('items')) == 1 17 | 18 | 19 | if __name__ == "__main__": 20 | unittest.main() 21 | -------------------------------------------------------------------------------- /readme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/readme -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/readme.txt -------------------------------------------------------------------------------- /setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from os import environ 3 | 4 | SAEPY_LOG_VERSION = '0.0.2' # 当前SAEpy-log版本 5 | APP_NAME = environ.get("APP_NAME", "") 6 | debug = not APP_NAME 7 | 8 | ##下面需要修改 9 | SITE_TITLE = u"博客标题" #博客标题 10 | SITE_TITLE2 = u"博客标题2" #显示在边栏上头(有些模板用不到) 11 | SITE_SUB_TITLE = u"一个简单的运行在SAE上的blog" #副标题 12 | KEYWORDS = u"起床,吃饭,工作,睡觉" #博客关键字 13 | SITE_DECR = u"这是运行在SAE上的个人博客,记录生活,记录工作。" #博客描述,给搜索引擎看 14 | ADMIN_NAME = u"admin" #发博文的作者 15 | NOTICE_MAIL = u"" #常用的,容易看到的接收提醒邮件,如QQ 邮箱,仅作收件用 16 | 17 | ###配置邮件发送信息,提醒邮件用的,必须正确填写,建议用Gmail 18 | MAIL_FROM = '' #xxx@gmail.com 19 | MAIL_SMTP = 'smtp.gmail.com' 20 | MAIL_PORT = 587 21 | MAIL_PASSWORD = 'xxx' #你的邮箱登录密码,用以发提醒邮件 22 | 23 | #放在网页底部的统计代码 24 | ANALYTICS_CODE = """ 25 | 29 | """ 30 | 31 | ##### 存放附件的地方 可选SAE Storage 或 七牛 32 | ## 1) 使用SAE Storage 服务(保存上传的附件),需在SAE管理面板创建, 33 | ## 注意,优先使用 SAE Storage,若用七牛下面值请留空值 34 | STORAGE_DOMAIN_NAME = "" #attachment 35 | 36 | ## 2) 七牛 注册可获永久10G空间和每月10G流量,注册地址 http://t.cn/z8h5lsg 37 | QN_AK = "" #七牛 ACCESS_KEY 38 | QN_SK = "" #七牛 SECRET_KEY 39 | QN_BUCKET = "" #空间名称 , 如 upload 40 | 41 | ###设置容易调用的jquery 文件 42 | JQUERY = "http://lib.sinaapp.com/js/jquery/1.6.2/jquery.min.js" 43 | #JQUERY = "http://code.jquery.com/jquery-1.6.2.min.js" 44 | #JQUERY = "/static/jquery-plugin/jquery-1.6.4.js" 45 | 46 | COPY_YEAR = '2012' #页脚的 © 2011 47 | 48 | MAJOR_DOMAIN = '%s.sinaapp.com' % APP_NAME #主域名,默认是SAE 的二级域名 49 | #MAJOR_DOMAIN = 'www.yourdomain.com' 50 | 51 | ##博客使用的主题,目前可选 default/octopress/octopress-disqus 52 | ##你也可以把自己喜欢的wp主题移植过来, 53 | #制作方法参见 http://saepy.sinaapp.com/t/49 54 | THEME = 'octopress' 55 | 56 | #使用disqus 评论系统,如果你使用就填 website shortname, 57 | #申请地址 http://disqus.com/ 58 | DISQUS_WEBSITE_SHORTNAME = '' 59 | 60 | ####友情链接列表,在管理后台也实现了管理,下面的链接列表仍然有效并排在前面 61 | LINK_BROLL = [ 62 | {"text": 'SAEpy blog', "url": 'http://saepy.sinaapp.com'}, 63 | {"text": 'Sina App Engine', "url": 'http://sae.sina.com.cn/'}, 64 | ] 65 | 66 | #当发表新博文时自动ping RPC服务,中文的下面三个差不多了 67 | XML_RPC_ENDPOINTS = [ 68 | 'http://blogsearch.google.com/ping/RPC2', 69 | 'http://rpc.pingomatic.com/', 70 | 'http://ping.baidu.com/ping/RPC2' 71 | ] 72 | 73 | ##如果要在本地测试则需要配置Mysql 数据库信息 74 | if debug: 75 | MYSQL_DB = 'app_saepy' 76 | MYSQL_USER = 'root' 77 | MYSQL_PASS = '123' 78 | MYSQL_HOST_M = '127.0.0.1' 79 | MYSQL_HOST_S = '127.0.0.1' 80 | MYSQL_PORT = '3306' 81 | 82 | ####除了修改上面的设置,你还需在SAE 后台开通下面几项服务: 83 | # 1 初始化 Mysql 84 | # 2 建立一个名为 attachment 的 Storage 85 | # 3 启用Memcache,初始化大小为1M的 mc,大小可以调,日后文章多了,PV多了可增加 86 | # 4 创建一个 名为 default 的 Task Queue 87 | # 详见 http://saepy.sinaapp.com/t/50 详细安装指南 88 | ############## 下面不建议修改 ########################### 89 | if debug: 90 | BASE_URL = 'http://127.0.0.1:8080' 91 | else: 92 | BASE_URL = 'http://%s'%MAJOR_DOMAIN 93 | 94 | LANGUAGE = 'zh-CN' 95 | COMMENT_DEFAULT_VISIBLE = 1 #0/1 #发表评论时是否显示 设为0时则需要审核才显示 96 | EACH_PAGE_POST_NUM = 7 #每页显示文章数 97 | EACH_PAGE_COMMENT_NUM = 10 #每页评论数 98 | RELATIVE_POST_NUM = 5 #显示相关文章数 99 | SHORTEN_CONTENT_WORDS = 150 #文章列表截取的字符数 100 | DESCRIPTION_CUT_WORDS = 100 #meta description 显示的字符数 101 | RECENT_COMMENT_NUM = 5 #边栏显示最近评论数 102 | RECENT_COMMENT_CUT_WORDS = 20 #边栏评论显示字符数 103 | LINK_NUM = 30 #边栏显示的友情链接数 104 | MAX_COMMENT_NUM_A_DAY = 10 #客户端设置Cookie 限制每天发的评论数 105 | 106 | PAGE_CACHE = not debug #本地没有Memcache 服务 107 | PAGE_CACHE_TIME = 3600*24 #默认页面缓存时间 108 | 109 | HOT_TAGS_NUM = 100 #右侧热门标签显示数 110 | 111 | MAX_IDLE_TIME = 5 #数据库最大空闲时间 SAE文档说是30 其实更小,设为5,没问题就不要改了 112 | -------------------------------------------------------------------------------- /setting.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/setting.pyc -------------------------------------------------------------------------------- /static/admin/style.css: -------------------------------------------------------------------------------- 1 | *{margin:0 auto} 2 | body{background:#231f20;color:#000;font-family:Georgia, "Times New Roman", Times, serif;font-size:.75em;margin-top:0} 3 | cite{font-style:normal} 4 | cite a{color:#f70;font-style:normal} 5 | code{background:#efefef;border:1px solid #dfdfdf;display:block;font-size:1.1em;line-height:1.3em;padding:10px} 6 | h2{font-size:1.5em;font-weight:400} 7 | p{line-height:1.5em;margin-top:20px;text-align:left} 8 | pre{background:#efefef;border:1px solid #dfdfdf;display:block;font-size:1.1em;line-height:1.3em;margin-top:20px;overflow:auto;padding:10px} 9 | textarea{font-family:Arial;font-size:1em} 10 | #wrap{background:#efefef;border:15px solid #4E4C4D;width:930px;padding-top:15px} 11 | #bannerwrap{text-align:center;width:830px} 12 | #contentwrap{width:830px} 13 | #contentwrap h1{background:#000;color:#fff;font-weight:400;margin-bottom:20px;padding:20px} 14 | #contentwrap h1 a{color:#fff} 15 | #content{float:right;width:550px} 16 | .content-margin-20{float:left;width:20px} 17 | #content h1 .description{color:#fff;font-size:.5em;font-weight:400} 18 | .post embed,.post img,.post object{display:block;margin-top:20px;max-width:496px} 19 | .post img,.post object{border:4px solid #dfdfdf;padding:3px} 20 | .post embed.alignleft,.post img.alignleft,.post object.alignleft{margin:0 10px 10px 0} 21 | .post embed.alignright,.post img.alignright,.post object.alignright{margin:0 0 10px 10px} 22 | .post ul,.post ol{line-height:1.5em;margin-top:20px} 23 | .postmetadata-top{margin-top:.5em;text-transform:uppercase} 24 | .postmetadata-bottom{margin-top:20px;text-transform:uppercase} 25 | .post table{background:#efefef;border:1px solid #dfdfdf;border-collapse:collapse;line-height:1.5em;width:100%;margin:20px auto 0} 26 | .post th,.post td{background:#efefef;border:1px solid #dfdfdf;padding:5px} 27 | #sidebar1wrap{float:left;width:260px} 28 | #sidebar1{background:#fff;margin-bottom:20px;width:220px;padding:20px} 29 | #sidebar1 ul{margin:0;padding:0} 30 | #sidebar1 ul.children a,#sidebar1 li.page_item ul li.page_item a{padding-left:20px} 31 | #sidebar1 ul.children ul.children a,#sidebar1 li.page_item ul li.page_item ul li.page_item a{padding-left:30px} 32 | #sidebar1 ul.children ul.children ul.children a,#sidebar1 li.page_item ul li.page_item ul li.page_item ul li.page_item a{padding-left:40px} 33 | #sidebar1 li{list-style:none} 34 | #sidebar1 a,#sidebar1 li.recentcomments{border-bottom:1px dotted #dfdfdf;color:#000;display:block;padding:5px 5px 5px 10px} 35 | #sidebar1 li.recentcomments a{border:none} 36 | #sidebar1 .widget_text{border-bottom:1px dotted #dfdfdf} 37 | #sidebar1 .textwidget{padding:5px 0} 38 | #sidebar1 .textwidget img{display:block} 39 | #sidebar1 .widget_text a,#sidebar1 .widget_text a img{border:none;padding:0} 40 | #sidebar1 .widget_text a:hover{background:none;color:#000} 41 | #sidebar1 li.widget_categories ul li{border-bottom:1px dotted #dfdfdf;display:block;padding:5px 5px 5px 10px} 42 | #sidebar1 li.widget_categories ul li a{border-bottom:none;display:inline;padding:0} 43 | #sidebar1 h2.small{border-bottom:1px dotted #000;color:#f70;font-size:1em;font-weight:700;margin-top:20px;padding-bottom:5px} 44 | #sidebar1 select{width:100%;margin:5px 0} 45 | #sidebar1 #tag_cloud h2{margin-bottom:10px} 46 | #sidebar1 #tag_cloud a{border:none;display:inline;padding:0 3px} 47 | #sidebar1 table{border-top:1px dotted #000;width:100%} 48 | #sidebar1 table a{border:none;color:#000;display:block;font-weight:700;padding:3px} 49 | #sidebar1 table th{color:#000;text-align:center} 50 | #sidebar1 table td{color:#dfdfdf;text-align:center} 51 | #sidebar1 caption{color:#f70;font-size:1em;font-weight:700;margin-bottom:10px;text-align:left} 52 | #comments,#respond{background:#fff;width:550px} 53 | ul#commentlist{list-style:none;margin-bottom:20px;padding:0 20px} 54 | ul#commentlist li.comment{border-top:1px dotted #dfdfdf;padding:20px 0} 55 | ul#commentlist ul.children{list-style:none;padding:0 0 0 20px} 56 | ul#commentlist ul.children li{border-top:1px dotted #dfdfdf;margin:10px 0 0;padding:10px 0 0} 57 | ul#commentlist li blockquote blockquote{margin-bottom:0} 58 | h2.comments,h2.respond{color:#000;padding:20px} 59 | h2.commentpages{margin-bottom:20px} 60 | small a.comment-edit-link{color:#f70;text-transform:uppercase} 61 | #commentlist div .avatar{float:left;border:4px solid #dfdfdf;margin:0 10px 10px 0;padding:3px} 62 | .vcard .fn,.vcard .says{font-size:1.5em} 63 | .comment-meta{font-size:.8em;margin-bottom:40px;text-transform:uppercase} 64 | .reply{margin-top:20px;text-align:left} 65 | #cancel-comment-reply-link{margin-left:20px} 66 | li.depth-1 #respond{margin:0 0 -20px -20px} 67 | li.depth-2 #respond{margin:0 0 -20px -40px} 68 | li.depth-3 #respond{margin:0 0 -20px -60px} 69 | li.depth-4 #respond{margin:0 0 -20px -80px} 70 | li.depth-5 #respond{margin:0 0 -20px -100px} 71 | #commentform{margin-bottom:20px;width:510px;padding:0 20px 20px} 72 | #commentform #commentbox{height:100px;width:502px} 73 | #commentform #submit{color:#000;margin-top:20px} 74 | #commentform small{text-transform:normal} 75 | .commentlinks{padding:5px} 76 | .commentlinks a{color:#000;padding:5px 10px} 77 | .commentlinks a:hover{background:#000;color:#fff} 78 | .commentlinks .current{background:#000;color:#fff;padding:5px 10px} 79 | #s{background:#efefef url(../img/mag.png) top left no-repeat;border:1px solid #000;color:#000;width:192px;padding:3px 3px 3px 25px} 80 | #footer{border-top:1px solid #000;clear:both;text-align:right;width:830px;padding:20px 0} 81 | .navlink a{background:#fff;color:#f70;display:block;margin-bottom:20px;text-align:center;padding:10px} 82 | .navlink-posts{background:#fff;color:#f70;display:block;text-align:center;margin:20px 0;padding:10px} 83 | .wp-smiley{border:none;display:inline;margin:0} 84 | .aligncenter,div.aligncenter{display:block;margin-left:auto;margin-right:auto} 85 | .alignleft{float:left} 86 | .alignright{float:right} 87 | .ggad_post_top_link{margin-bottom:8px} 88 | .bigbtn{cursor:pointer;padding:4px} 89 | a,.post a{color:#f70;text-decoration:none} 90 | a:hover,.post a:hover{color:#000;text-decoration:none} 91 | #bannerwrap img,#banner-custom img{border:4px solid #000;margin-bottom:20px;max-width:816px;padding:3px} 92 | .post,#commentpages{background:#fff;margin-bottom:20px;padding:20px} 93 | .post blockquote,ul#commentlist li blockquote{border-left:4px solid #dfdfdf;margin-top:20px;padding:1em} 94 | .post blockquote p,ul#commentlist li blockquote p{margin:0} 95 | .post h2 a,.comment-meta a,#footer a,.navlink a:hover,.navlink-posts a:hover{color:#000} 96 | .post h2 a:hover,#sidebar1 a:hover,#sidebar1 li.widget_categories ul li:hover,#sidebar1 li.widget_categories ul li:hover a,.comment-meta a.comment-edit-link,#commentform a{color:#f70} 97 | .post input,#commentform input,#commentform textarea{background:#fff;border:1px solid #dfdfdf;color:#000;padding:3px} 98 | .post input[type=submit],#sidebar1 #calendar{margin-top:20px} 99 | #sidebar1 small,#footer small,.uppercase{text-transform:uppercase} 100 | #sidebar1 #calendar h2,#searchsubmit{display:none} 101 | .btn {cursor:pointer;} 102 | .red {color:red;} 103 | .submitbtn{cursor:pointer} 104 | -------------------------------------------------------------------------------- /static/facefiles/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/static/facefiles/b.png -------------------------------------------------------------------------------- /static/facefiles/bl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/static/facefiles/bl.png -------------------------------------------------------------------------------- /static/facefiles/br.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/static/facefiles/br.png -------------------------------------------------------------------------------- /static/facefiles/closelabel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/static/facefiles/closelabel.gif -------------------------------------------------------------------------------- /static/facefiles/facebox.css: -------------------------------------------------------------------------------- 1 | #facebox .b{background:url(b.png);}#facebox .tl{background:url(tl.png);}#facebox .tr{background:url(tr.png);}#facebox .bl{background:url(bl.png);}#facebox .br{background:url(br.png);}#facebox{position: absolute; width: 100%; top: 0; left: 0; z-index: 100; text-align: left;}#facebox .popup{position: relative;}#facebox table{margin: auto; border-collapse: collapse;}#facebox .body{padding: 10px; background: #fff; width: 370px;}#facebox .loading{text-align: center;}#facebox .image{text-align: center;}#facebox img{border: 0;}#facebox .footer{border-top: 1px solid #DDDDDD; padding-top: 5px; margin-top: 10px; text-align: right; background-color:#fff;}#facebox .tl, #facebox .tr, #facebox .bl, #facebox .br{height: 10px; width: 10px; overflow: hidden; padding: 0;}#facebox td{background:#fff;} -------------------------------------------------------------------------------- /static/facefiles/facebox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Facebox (for jQuery) 3 | * version: 1.1 (03/01/2008) 4 | * @requires jQuery v1.2 or later 5 | * 6 | * Examples at http://famspam.com/facebox/ 7 | * 8 | * Licensed under the MIT: 9 | * http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * Copyright 2007, 2008 Chris Wanstrath [ chris@ozmm.org ] 12 | * 13 | * Usage: 14 | * 15 | * jQuery(document).ready(function() { 16 | * jQuery('a[rel*=facebox]').facebox() 17 | * }) 18 | * 19 | * Terms 20 | * Loads the #terms div in the box 21 | * 22 | * Terms 23 | * Loads the terms.html page in the box 24 | * 25 | * Terms 26 | * Loads the terms.gif image in the box 27 | * 28 | * 29 | * You can also use it programmatically: 30 | * 31 | * jQuery.facebox('some html') 32 | * 33 | * This will open a facebox with "some html" as the content. 34 | * 35 | * jQuery.facebox(function() { ajaxes }) 36 | * 37 | * This will show a loading screen before the passed function is called, 38 | * allowing for a better ajax experience. 39 | * 40 | */ 41 | (function($) { 42 | $.facebox = function(data, klass) { 43 | $.facebox.init() 44 | $.facebox.loading() 45 | $.isFunction(data) ? data.call($) : $.facebox.reveal(data, klass) 46 | } 47 | 48 | $.facebox.settings = { 49 | loading_image : '/static/facefiles/loading.gif', 50 | close_image : '/static/facefiles/closelabel.gif', 51 | image_types : [ 'png', 'jpg', 'jpeg', 'gif' ], 52 | facebox_html : '\ 53 | ' 80 | } 81 | 82 | $.facebox.loading = function() { 83 | if ($('#facebox .loading').length == 1) return true 84 | 85 | $('#facebox .content').empty() 86 | $('#facebox .body').children().hide().end(). 87 | append('
') 88 | 89 | var pageScroll = $.facebox.getPageScroll() 90 | $('#facebox').css({ 91 | top: pageScroll[1] + ($.facebox.getPageHeight() / 10), 92 | left: pageScroll[0] 93 | }).show() 94 | 95 | $(document).bind('keydown.facebox', function(e) { 96 | if (e.keyCode == 27) $.facebox.close() 97 | }) 98 | } 99 | 100 | $.facebox.reveal = function(data, klass) { 101 | if (klass) $('#facebox .content').addClass(klass) 102 | $('#facebox .content').append(data) 103 | $('#facebox .loading').remove() 104 | $('#facebox .body').children().fadeIn('normal') 105 | } 106 | 107 | $.facebox.close = function() { 108 | $(document).trigger('close.facebox') 109 | return false 110 | } 111 | 112 | $(document).bind('close.facebox', function() { 113 | $(document).unbind('keydown.facebox') 114 | $('#facebox').fadeOut(function() { 115 | $('#facebox .content').removeClass().addClass('content') 116 | }) 117 | }) 118 | 119 | $.fn.facebox = function(settings) { 120 | $.facebox.init(settings) 121 | 122 | var image_types = $.facebox.settings.image_types.join('|') 123 | image_types = new RegExp('\.' + image_types + '$', 'i') 124 | 125 | function click_handler() { 126 | $.facebox.loading(true) 127 | 128 | // support for rel="facebox[.inline_popup]" syntax, to add a class 129 | var klass = this.rel.match(/facebox\[\.(\w+)\]/) 130 | if (klass) klass = klass[1] 131 | 132 | // div 133 | if (this.href.match(/#/)) { 134 | var url = window.location.href.split('#')[0] 135 | var target = this.href.replace(url,'') 136 | $.facebox.reveal($(target).clone().show(), klass) 137 | 138 | // image 139 | } else if (this.href.match(image_types)) { 140 | var image = new Image() 141 | image.onload = function() { 142 | $.facebox.reveal('
', klass) 143 | } 144 | image.src = this.href 145 | 146 | // ajax 147 | } else { 148 | $.get(this.href, function(data) { $.facebox.reveal(data, klass) }) 149 | } 150 | 151 | return false 152 | } 153 | 154 | this.click(click_handler) 155 | return this 156 | } 157 | 158 | $.facebox.init = function(settings) { 159 | if ($.facebox.settings.inited) { 160 | return true 161 | } else { 162 | $.facebox.settings.inited = true 163 | } 164 | 165 | if (settings) $.extend($.facebox.settings, settings) 166 | $('body').append($.facebox.settings.facebox_html) 167 | 168 | var preload = [ new Image(), new Image() ] 169 | preload[0].src = $.facebox.settings.close_image 170 | preload[1].src = $.facebox.settings.loading_image 171 | 172 | $('#facebox').find('.b:first, .bl, .br, .tl, .tr').each(function() { 173 | preload.push(new Image()) 174 | preload.slice(-1).src = $(this).css('background-image').replace(/url\((.+)\)/, '$1') 175 | }) 176 | 177 | $('#facebox .close').click($.facebox.close) 178 | $('#facebox .close_image').attr('src', $.facebox.settings.close_image) 179 | } 180 | 181 | // getPageScroll() by quirksmode.com 182 | $.facebox.getPageScroll = function() { 183 | var xScroll, yScroll; 184 | if (self.pageYOffset) { 185 | yScroll = self.pageYOffset; 186 | xScroll = self.pageXOffset; 187 | } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict 188 | yScroll = document.documentElement.scrollTop; 189 | xScroll = document.documentElement.scrollLeft; 190 | } else if (document.body) {// all other Explorers 191 | yScroll = document.body.scrollTop; 192 | xScroll = document.body.scrollLeft; 193 | } 194 | return new Array(xScroll,yScroll) 195 | } 196 | 197 | // adapter from getPageSize() by quirksmode.com 198 | $.facebox.getPageHeight = function() { 199 | var windowHeight 200 | if (self.innerHeight) { // all except Explorer 201 | windowHeight = self.innerHeight; 202 | } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode 203 | windowHeight = document.documentElement.clientHeight; 204 | } else if (document.body) { // other Explorers 205 | windowHeight = document.body.clientHeight; 206 | } 207 | return windowHeight 208 | } 209 | })(jQuery); 210 | -------------------------------------------------------------------------------- /static/facefiles/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/static/facefiles/loading.gif -------------------------------------------------------------------------------- /static/facefiles/tl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/static/facefiles/tl.png -------------------------------------------------------------------------------- /static/facefiles/tr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/static/facefiles/tr.png -------------------------------------------------------------------------------- /static/highlight/sunburst.min.css: -------------------------------------------------------------------------------- 1 | pre code{white-space:0;word-wrap:break-word;display:block;background:#000;color:#f8f8f8;margin-bottom:20px;margin-top:20px;padding:.5em}pre .comment,pre .template_comment,pre .javadoc{color:#aeaeae;font-style:italic}pre .keyword,pre .ruby .function .keyword{color:#E28964}pre .function .keyword,pre .sub .keyword,pre .method,pre .list .title{color:#99CF50}pre .string,pre .tag .value,pre .cdata,pre .filter .argument,pre .attr_selector,pre .apache .cbracket,pre .date,pre .tex .command{color:#65B042}pre .subst{color:#DAEFA3}pre .regexp{color:#E9C062}pre .function .title,pre .sub .identifier,pre .pi,pre .tag,pre .tag .keyword,pre .decorator,pre .ini .title,pre .shebang,pre .input_number{color:#89BDFF}pre .class .title,pre .haskell .label,pre .smalltalk .class,pre .javadoctag,pre .yardoctag,pre .phpdoc{text-decoration:underline}pre .symbol,pre .ruby .symbol .string,pre .ruby .symbol .keyword,pre .ruby .symbol .keymethods,pre .number{color:#3387CC}pre .params,pre .variable{color:#3E87E3}pre .css .tag,pre .rules .property,pre .pseudo,pre .tex .special{color:#CDA869}pre .css .class{color:#9B703F}pre .rules .keyword{color:#C5AF75}pre .rules .value{color:#CF6A4C}pre .css .id{color:#8B98AB}pre .annotation,pre .apache .sqbracket,pre .nginx .built_in{color:#9B859D}pre .preprocessor{color:#8996A8}pre .hexcolor,pre .css .value .number{color:#DD7B3B}pre .css .function{color:#DAD085}pre .diff .header,pre .chunk,pre .tex .formula{background-color:#0E2231;color:#F8F8F8;font-style:italic}pre .diff .change{background-color:#4A410D;color:#F8F8F8}pre .addition{background-color:#253B22;color:#F8F8F8}pre .deletion{background-color:#420E09;color:#F8F8F8} -------------------------------------------------------------------------------- /static/img/grey.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ego008/saepy-log/eb0afaab6c9547227994ebc2b68307a77a6476e9/static/img/grey.gif -------------------------------------------------------------------------------- /static/jquery-plugin/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Cookie plugin 3 | * 4 | * Copyright (c) 2010 Klaus Hartl (stilbuero.de) 5 | * Dual licensed under the MIT and GPL licenses: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * http://www.gnu.org/licenses/gpl.html 8 | * 9 | */ 10 | jQuery.cookie = function (key, value, options) { 11 | 12 | // key and at least value given, set cookie... 13 | if (arguments.length > 1 && String(value) !== "[object Object]") { 14 | options = jQuery.extend({}, options); 15 | 16 | if (value === null || value === undefined) { 17 | options.expires = -1; 18 | } 19 | 20 | if (typeof options.expires === 'number') { 21 | var days = options.expires, t = options.expires = new Date(); 22 | t.setDate(t.getDate() + days); 23 | } 24 | 25 | value = String(value); 26 | 27 | return (document.cookie = [ 28 | encodeURIComponent(key), '=', 29 | options.raw ? value : encodeURIComponent(value), 30 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 31 | options.path ? '; path=' + options.path : '', 32 | options.domain ? '; domain=' + options.domain : '', 33 | options.secure ? '; secure' : '' 34 | ].join('')); 35 | } 36 | 37 | // key and possibly options given, get cookie... 38 | options = value || {}; 39 | var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent; 40 | return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null; 41 | }; 42 | -------------------------------------------------------------------------------- /static/jquery-plugin/jquery.upload-1.0.2.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery.upload v1.0.2 3 | * 4 | * Copyright (c) 2010 lagos 5 | * Dual licensed under the MIT and GPL licenses. 6 | * 7 | * http://lagoscript.org 8 | */ 9 | (function(b){function m(e){return b.map(n(e),function(d){return''}).join("")}function n(e){function d(c,f){a.push({name:c,value:f})}if(b.isArray(e))return e;var a=[];if(typeof e==="object")b.each(e,function(c){b.isArray(this)?b.each(this,function(){d(c,this)}):d(c,b.isFunction(this)?this():this)});else typeof e==="string"&&b.each(e.split("&"),function(){var c=b.map(this.split("="),function(f){return decodeURIComponent(f.replace(/\+/g," "))}); 10 | d(c[0],c[1])});return a}function o(e,d){var a;a=b(e).contents().get(0);if(b.isXMLDoc(a)||a.XMLDocument)return a.XMLDocument||a;a=b(a).find("body").html();switch(d){case "xml":a=a;if(window.DOMParser)a=(new DOMParser).parseFromString(a,"application/xml");else{var c=new ActiveXObject("Microsoft.XMLDOM");c.async=false;c.loadXML(a);a=c}break;case "json":a=window.eval("("+a+")");break}return a}var p=0;b.fn.upload=function(e,d,a,c){var f=this,g,j,h;h="jquery_upload"+ ++p;var k=b('