21 | 文章相关的api API文档
22 | 模型定义
23 | blog.blog.article.article.Article
24 |
25 |
26 |
27 | 字段名 |
28 | 类型 |
29 | 是否必须 |
30 | 是否可为空值 |
31 | 默认值 |
32 | 描述 |
33 | 示例 |
34 |
35 |
36 |
37 |
38 | title |
39 | String |
40 | 是 |
41 | 否 |
42 | |
43 | 标题 |
44 | |
45 |
46 |
47 | id |
48 | String |
49 | 否 |
50 | 否 |
51 | 20190126144023 |
52 | 每篇文章的唯一id,日期字符串的形式表示 |
53 | |
54 |
55 |
56 | tags |
57 | Tags |
58 | 是 |
59 | 否 |
60 | |
61 | 关键字 |
62 | ["python", "apistellar"] |
63 |
64 |
65 | description |
66 | String |
67 | 否 |
68 | 否 |
69 | |
70 | 描述信息 |
71 | |
72 |
73 |
74 | author |
75 | String |
76 | 否 |
77 | 否 |
78 | 夏洛之枫 |
79 | 作者信息 |
80 | |
81 |
82 |
83 | feature |
84 | Boolean |
85 | 否 |
86 | 否 |
87 | False |
88 | 是否为精品 |
89 | |
90 |
91 |
92 | created_at |
93 | Timestamp |
94 | 否 |
95 | 否 |
96 | 1548484823.313708 |
97 | 创建时间 |
98 | |
99 |
100 |
101 | updated_at |
102 | Timestamp |
103 | 否 |
104 | 否 |
105 | 1548484823.313737 |
106 | 更新时间 |
107 | |
108 |
109 |
110 | show |
111 | Boolean |
112 | 否 |
113 | 否 |
114 | True |
115 | 是否在文章列表中展示 |
116 | |
117 |
118 |
119 | article |
120 | String |
121 | 否 |
122 | 否 |
123 | |
124 | 文章正文 |
125 | |
126 |
127 |
128 |
129 | 接口定义
130 | 1. 导入文章
131 | URL: /import
132 | 方法: GET
133 | 返回信息
134 | 返回描述
135 | 导入文章页面
136 | 2. 检查用户名和密码是否正确
137 | URL: /check
138 | 方法: POST
139 | 表单参数:
140 |
141 |
142 |
143 | 参数名 |
144 | 类型 |
145 | 是否必须 |
146 | 默认值 |
147 | 描述 |
148 | 示例 |
149 |
150 |
151 |
152 |
153 | article |
154 | blog.blog.article.article.Article |
155 | 是 |
156 | |
157 | |
158 | {"title": "xxx"} |
159 |
160 |
161 | username |
162 | str |
163 | 是 |
164 | |
165 | 用户名 |
166 | test |
167 |
168 |
169 | password |
170 | str |
171 | 是 |
172 | |
173 | 密码 |
174 | 12345 |
175 |
176 |
177 | ref |
178 | str |
179 | 是 |
180 | |
181 | 从哪里跳过来的 |
182 | |
183 |
184 |
185 |
186 | 返回信息
187 | 返回类型
188 | str
189 | 返回描述
190 | 返回网页
191 | 3. 检查用户名密码是否正确,返回检查结果
192 | URL: /load
193 | 方法: GET
194 | 查询参数:
195 |
196 |
197 |
198 | 参数名 |
199 | 类型 |
200 | 是否必须 |
201 | 默认值 |
202 | 描述 |
203 | 示例 |
204 |
205 |
206 |
207 |
208 | username |
209 | str |
210 | 是 |
211 | |
212 | 用户名 |
213 | test |
214 |
215 |
216 | password |
217 | str |
218 | 是 |
219 | |
220 | 密码 |
221 | 12345 |
222 |
223 |
224 |
225 | 返回信息
226 | 返回示例
227 | 示例1
228 | {"code": 0, "data": null}
229 |
230 |
231 | 示例2
232 | {"code": 401, "message": "密码错误"}
233 |
234 |
235 | 返回响应码
236 |
237 |
238 |
239 | 响应码 |
240 | 描述 |
241 |
242 |
243 |
244 |
245 | 0 |
246 | 返回成功 |
247 |
248 |
249 | 401 |
250 | 密码错误 |
251 |
252 |
253 |
254 | 4. 用于上传文章
255 | URL: /upload
256 | 方法: POST
257 | json请求体
258 | 请求描述
259 | 文章相关信息
260 | 模型类型
261 | blog.blog.article.article.Article
262 | 返回信息
263 | 返回描述
264 | 返回上传页面继续上传
265 | 5. 导出文章
266 | URL: /export
267 | 方法: GET
268 | 查询参数:
269 |
270 |
271 |
272 | 参数名 |
273 | 类型 |
274 | 是否必须 |
275 | 默认值 |
276 | 描述 |
277 | 示例 |
278 |
279 |
280 |
281 |
282 | ids |
283 | str |
284 | 是 |
285 | |
286 | 要导出的文章id,使用,连接成字符串 |
287 | 20181010111111,20181020111111 |
288 |
289 |
290 | code |
291 | str |
292 | 否 |
293 | None |
294 | 后端生成的用于验证的code |
295 | |
296 |
297 |
298 |
299 | 返回信息
300 | 返回描述
301 | 导出生成的压缩包
302 | 6. 这个接口用于直接在网页上修改文章内容
303 | URL: /modify
304 | 方法: POST
305 | 表单参数:
306 |
307 |
308 |
309 | 参数名 |
310 | 类型 |
311 | 是否必须 |
312 | 默认值 |
313 | 描述 |
314 | 示例 |
315 |
316 |
317 |
318 |
319 | img_url |
320 | str |
321 | 是 |
322 | |
323 | 首图地址 |
324 | http://www.csdn.....jpg |
325 |
326 |
327 | article |
328 | blog.blog.article.article.Article |
329 | 是 |
330 | |
331 | 文章对象 |
332 | |
333 |
334 |
335 |
336 | 返回信息
337 | 返回示例
338 | 示例1
339 | {"code": 0, "data": null}
340 |
341 |
342 | 示例2
343 | {"code": 401, "message": "Login required!"}
344 |
345 |
346 | 返回响应码
347 |
348 |
349 |
350 | 响应码 |
351 | 描述 |
352 |
353 |
354 |
355 |
356 | 0 |
357 | 返回成功 |
358 |
359 |
360 | 401 |
361 | Login required! |
362 |
363 |
364 |
365 | 7. 打开文章编辑页面
366 | URL: /edit
367 | 方法: GET
368 | 查询参数:
369 |
370 |
371 |
372 | 参数名 |
373 | 类型 |
374 | 是否必须 |
375 | 默认值 |
376 | 描述 |
377 | 示例 |
378 |
379 |
380 |
381 |
382 | id |
383 | str |
384 | 是 |
385 | |
386 | 文章的id |
387 | 20111111111111 |
388 |
389 |
390 |
391 | 返回信息
392 | 返回描述
393 | 如果登录了,跳转到编辑页面,否则,跳转到登录页。
394 | 8. 编辑之后更新文章内容
395 | URL: /update
396 | 方法: POST
397 | 表单参数:
398 |
399 |
400 |
401 | 参数名 |
402 | 类型 |
403 | 是否必须 |
404 | 默认值 |
405 | 描述 |
406 | 示例 |
407 |
408 |
409 |
410 |
411 | article |
412 | blog.blog.article.article.Article |
413 | 是 |
414 | |
415 | 文章对象 |
416 | |
417 |
418 |
419 |
420 | 返回信息
421 | 返回描述
422 | 如果登录了,跳转到首页,否则,跳转到登录页
423 | 9. 删除文章接口
424 | URL: /delete
425 | 方法: GET
426 | 查询参数:
427 |
428 |
429 |
430 | 参数名 |
431 | 类型 |
432 | 是否必须 |
433 | 默认值 |
434 | 描述 |
435 | 示例 |
436 |
437 |
438 |
439 |
440 | id |
441 | str |
442 | 是 |
443 | |
444 | 要删除的文章id |
445 | 19911111111111 |
446 |
447 |
448 |
449 | 返回信息
450 | 返回描述
451 | 如果登录了,跳转到首页,否则跳转到登录页。
452 | 10. 获取文章
453 | URL: /article
454 | 方法: GET
455 | 查询参数:
456 |
457 |
458 |
459 | 参数名 |
460 | 类型 |
461 | 是否必须 |
462 | 默认值 |
463 | 描述 |
464 | 示例 |
465 |
466 |
467 |
468 |
469 | id |
470 | str |
471 | 是 |
472 | |
473 | 要获取的文章id |
474 | |
475 |
476 |
477 |
478 | 返回信息
479 | 返回类型
480 | blog.blog.article.article.Article
481 | 返回描述
482 | 获取到的文章对象
483 | 11. 获取关于我的文章
484 | URL: /me
485 | 方法: GET
486 | 查询参数:
487 |
488 |
489 |
490 | 参数名 |
491 | 类型 |
492 | 是否必须 |
493 | 默认值 |
494 | 描述 |
495 | 示例 |
496 |
497 |
498 |
499 |
500 | code |
501 | str |
502 | 是 |
503 | |
504 | 后端生成的用于验证的code |
505 | |
506 |
507 |
508 |
509 | 返回信息
510 | 返回类型
511 | blog.blog.article.article.Article
512 | 返回描述
513 | 获取到的文章对象
514 | 12. 获取我的联系方式
515 | URL: /contact
516 | 方法: GET
517 | 返回信息
518 | 返回类型
519 | blog.blog.article.article.Article
520 | 返回描述
521 | 获取到的文章对象
522 | 13. 首页展示接口
523 | URL: /show
524 | 方法: GET
525 | 查询参数:
526 |
527 |
528 |
529 | 参数名 |
530 | 类型 |
531 | 是否必须 |
532 | 默认值 |
533 | 描述 |
534 | 示例 |
535 |
536 |
537 |
538 |
539 | searchField |
540 | str |
541 | 否 |
542 | |
543 | 搜索关键词 |
544 | python |
545 |
546 |
547 | fulltext |
548 | bool |
549 | 否 |
550 | True |
551 | 是否全文搜索 |
552 | true/false |
553 |
554 |
555 | _from |
556 | int |
557 | 否 |
558 | 0 |
559 | 从第几篇文章开始搜 |
560 | 0 |
561 |
562 |
563 | size |
564 | int |
565 | 否 |
566 | 10 |
567 | 每页大小 |
568 | 10 |
569 |
570 |
571 |
572 | 返回信息
573 | 返回示例
574 | {
575 | "count": 10,
576 | "articles": [...],
577 | "feature_articles": [..],
578 | "tags": ["python", "ubuntu"...]
579 | }
580 |
581 |
582 | 14. 截图api
583 | URL: /cut
584 | 方法: GET
585 | 查询参数:
586 |
587 |
588 |
589 | 参数名 |
590 | 类型 |
591 | 是否必须 |
592 | 默认值 |
593 | 描述 |
594 | 示例 |
595 |
596 |
597 |
598 |
599 | url |
600 | str |
601 | 是 |
602 | |
603 | 要截图的地址 |
604 | |
605 |
606 |
607 | top |
608 | int |
609 | 否 |
610 | 0 |
611 | 截图区域的top |
612 | |
613 |
614 |
615 | left |
616 | int |
617 | 否 |
618 | 0 |
619 | 截图区域的left |
620 | |
621 |
622 |
623 | width |
624 | int |
625 | 否 |
626 | 1024 |
627 | 截图区域的width |
628 | |
629 |
630 |
631 | height |
632 | int |
633 | 否 |
634 | 768 |
635 | 截图区域的height |
636 | |
637 |
638 |
639 |
640 | 返回信息
641 | 返回描述
642 | 重定向到截图的静态地址
643 |
644 |
645 |
--------------------------------------------------------------------------------
/docs/我的博客API文档/index.md:
--------------------------------------------------------------------------------
1 | # 我的博客API文档
2 |
3 | 1. [欢迎页](welcome/欢迎页.md)
4 | 2. [文章相关](article/文章相关.md)
5 |
--------------------------------------------------------------------------------
/docs/我的博客API文档/index.md.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ()\n%s" % (
83 | img_url, h2t.handle(article.article)
84 | )
85 | await article.update()
86 |
87 | async def update(self, article):
88 | """
89 | 更新文章
90 | :param article:
91 | :return:
92 | """
93 | await article.update()
94 |
95 | async def delete(self, article):
96 | """
97 | 删除文章
98 | :param article:
99 | :return:
100 | """
101 | await article.remove()
102 |
103 | async def about(self, id):
104 | """
105 | 返回或者生成关于我和我的联系方式文章模板
106 | :param id:
107 | :return:
108 | """
109 | article = await Article.load(id=id)
110 | if not article:
111 | article.id = id
112 | article.author = self.settings.get("AUTHOR")
113 | article.tags = [id]
114 | article.description = id
115 | article.feature = False
116 | article.article = id
117 | article.title = id
118 | article.show = False
119 | article.format()
120 | await article.save()
121 |
122 | article = article.to_dict()
123 | article["first_img"] = self._get_image(article["article"])
124 | article["article"] = markdown.markdown(
125 | article["article"], extensions=['markdown.extensions.extra'])
126 | return article
127 |
128 | async def show(self, searchField, _from, size, fulltext):
129 | """
130 | 首页展示
131 | :param searchField:
132 | :param _from:
133 | :param size:
134 | :param fulltext:
135 | :return:
136 | """
137 | # 获取精品文档
138 | feature_articles = await Article.search(
139 | searchField, _from=_from, size=size,
140 | fulltext=fulltext, feature=True, show=True)
141 | self._enrich_first_img(feature_articles)
142 |
143 | # 获取首页文档
144 | articles = await Article.search(
145 | searchField, _from=_from, size=size, fulltext=fulltext, show=True)
146 | self._enrich_first_img(articles)
147 | # 获取全部tags
148 | count, tags = await Article.get_total_tags()
149 |
150 | return {
151 | "count": count,
152 | "articles": articles,
153 | "feature_articles": feature_articles,
154 | "tags": list(sorted(tags.items(), key=lambda x: x[1], reverse=True))
155 | }
156 |
157 | async def cut(self, url, top, left, width, height):
158 | """
159 | 按指定位置尺寸切网页
160 | :param url:
161 | :param top:
162 | :param left:
163 | :param width:
164 | :param height:
165 | :return:
166 | """
167 |
168 | sh = hashlib.sha1(url.encode())
169 | sh.update(bytes(str(top), encoding="utf-8"))
170 | sh.update(bytes(str(left), encoding="utf-8"))
171 | sh.update(bytes(str(width), encoding="utf-8"))
172 | sh.update(bytes(str(height), encoding="utf-8"))
173 | save_name = sh.hexdigest()[:10] + ".png"
174 | save_name = os.path.join(
175 | settings["PROJECT_PATH"], "static/temp/", save_name)
176 | loop = asyncio.get_event_loop()
177 | await loop.run_in_executor(
178 | self.executor,
179 | self._cut,
180 | url, save_name, top, left, width, height)
181 | return save_name
182 |
183 | @classmethod
184 | def _enrich_first_img(cls, articles):
185 | for index in range(len(articles)):
186 | article = articles[index].to_dict()
187 | article["first_img"] = cls._get_image(article.pop("article"))
188 | articles[index] = article
189 |
190 | @staticmethod
191 | def _get_image(body):
192 | try:
193 | image_part = body[:body.index("\n")]
194 | except ValueError:
195 | image_part = body
196 | mth = re.search(r"!\[.*?\]\((.*?)\)", image_part)
197 | return mth.group(1) if mth else ""
198 |
199 | @cache_method(3600)
200 | def _cut(self, url, save_name, top=0, left=0, width=1024, height=768):
201 | os.system(("%s " * 8) % (
202 | self.phantomjs_path, self.js_path, url,
203 | save_name, top, left, width, height))
204 |
--------------------------------------------------------------------------------
/src/blog/blog/exchange/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/blog/blog/exchange/solo.py:
--------------------------------------------------------------------------------
1 | from apistellar import Solo
2 |
3 | from blog.blog.article.article_exporter import ArticleExporter
4 |
5 |
6 | class Exchange(Solo):
7 | """
8 | 将网页转换pdf
9 | """
10 | def __init__(self, input_path, output_path, **kwargs):
11 | self.input_path = input_path
12 | self.output_path = output_path
13 | super(Exchange, self).__init__(**kwargs)
14 |
15 | async def setup(self):
16 | """
17 | 初始化
18 | :return:
19 | """
20 |
21 | async def run(self):
22 | """
23 | 业务逻辑
24 | :return:
25 | """
26 | with open(self.input_path) as f:
27 | await ArticleExporter.save_as_pdf(f.read(), self.output_path)
28 |
29 | async def teardown(self):
30 | """
31 | 回收资源
32 | :return:
33 | """
34 |
35 | @classmethod
36 | def enrich_parser(cls, sub_parser):
37 | """
38 | 自定义命令行参数,若定义了,则可通过__init__获取
39 | 注意在__init__中使用kwargs来保留其它参数,并调用父类的__init__
40 | :param sub_parser:
41 | :return:
42 | """
43 | sub_parser.add_argument("-i", "--input-path")
44 | sub_parser.add_argument("-o", "--output-path")
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/blog/blog/import_/__init__.py:
--------------------------------------------------------------------------------
1 | import re
2 | import glob
3 | import time
4 | from logging import getLogger
5 |
6 | from os.path import join, basename
7 | from apistellar import Solo, settings
8 |
9 | from ..article.article import Article
10 |
11 |
12 | logger = getLogger("import")
13 |
14 |
15 | class Import(Solo):
16 |
17 | def __init__(self, paths, **kwargs):
18 | self.paths = paths
19 | super(Import, self).__init__(**kwargs)
20 |
21 | async def setup(self):
22 | """
23 | 初始化
24 | :return:
25 | """
26 |
27 | async def run(self):
28 | """
29 | 业务逻辑
30 | :return:
31 | """
32 | for path in self.paths:
33 | for filename in glob.glob(join(path, "*")):
34 | await self.insert(filename)
35 |
36 | @classmethod
37 | async def insert(cls, filename):
38 | title = basename(filename).replace(".md", "")
39 | article = await Article.load(title=title)
40 |
41 | if not article:
42 | article.title = title
43 | lines = open(filename, encoding="utf-8").readlines()
44 | article.tags = cls.retrieve("tags", lines) or []
45 | article.description = cls.retrieve("description", lines)
46 | article.title = cls.retrieve("title", lines) or title
47 | article.author = cls.retrieve("author", lines) or settings["AUTHOR"]
48 | article.article = "".join(lines)
49 | article.format()
50 | await article.save()
51 | time.sleep(1)
52 | logger.debug(f"Import {filename} to db. ")
53 | else:
54 | logger.info(f"Article {filename} exist. ")
55 | return article
56 |
57 | @staticmethod
58 | def retrieve(word, article):
59 | regex = re.compile(r"\[comment\]: <%s> \((.+?)\)" % word)
60 | for line in article[:]:
61 | mth = regex.search(line)
62 | if mth:
63 | article.remove(line)
64 | return mth.group(1)
65 | return ""
66 |
67 | async def teardown(self):
68 | """
69 | 回收资源
70 | :return:
71 | """
72 |
73 | @classmethod
74 | def enrich_parser(cls, sub_parser):
75 | """
76 | 自定义命令行参数,若定义了,则可通过__init__获取
77 | 注意在__init__中使用kwargs来保留其它参数
78 | :param sub_parser:
79 | :return:
80 | """
81 | sub_parser.add_argument("paths", nargs="+", help="目录地址")
82 |
83 |
--------------------------------------------------------------------------------
/src/blog/blog/lib/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sqlite3
3 | import logging
4 |
5 | from pyaop import AOP, Proxy
6 |
7 | from apistellar import settings
8 | from apistellar.helper import cache_classproperty
9 | from apistellar.persistence import DriverMixin, proxy, contextmanager
10 |
11 | logger = logging.getLogger("sql")
12 |
13 |
14 | class SqliteProxy(Proxy):
15 | proxy_methods = ["execute"]
16 |
17 |
18 | def execute_before(self, *args, **kwargs):
19 | logger.debug(f"Execute sql: `{args[0]}` args: `{args[1]}`")
20 |
21 |
22 | class SqliteDriverMixin(DriverMixin):
23 |
24 | INIT_SQL_FILE = "blog.sql"
25 | DB_PATH = "db/blog"
26 |
27 | store = None # type: sqlite3.Cursor
28 |
29 | @cache_classproperty
30 | def init_sqlite(cls):
31 | project_path = settings["PROJECT_PATH"]
32 | os.makedirs(os.path.join(
33 | project_path, os.path.dirname(cls.DB_PATH)), exist_ok=True)
34 | table_initialize = open(
35 | os.path.join(project_path, cls.INIT_SQL_FILE)).read()
36 | conn = sqlite3.connect(
37 | os.path.join(project_path, cls.DB_PATH))
38 | cur = conn.cursor()
39 | try:
40 | cur.execute(table_initialize)
41 | except sqlite3.OperationalError as e:
42 | pass
43 | return conn, cur
44 |
45 | @classmethod
46 | @contextmanager
47 | def get_store(cls, self_or_cls, **callargs):
48 | conn, cur = cls.init_sqlite
49 | with super(SqliteDriverMixin, cls).get_store(
50 | self_or_cls, **callargs) as self_or_cls:
51 | cur = conn.cursor()
52 | if hasattr(self_or_cls, "_need_proxy") \
53 | and self_or_cls._need_proxy("store"):
54 | store = SqliteProxy(
55 | cur, before=[AOP.Hook(execute_before, ["execute"])])
56 | self_or_cls = proxy(self_or_cls, prop_name="store", prop=store)
57 | try:
58 | yield self_or_cls
59 | finally:
60 | conn.commit()
61 |
62 |
--------------------------------------------------------------------------------
/src/blog/blog/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | import os
3 | import time
4 | import random
5 |
6 | from apistellar import settings
7 |
8 |
9 | def code_generator(interval):
10 | project_path = settings["PROJECT_PATH"]
11 | code = None
12 |
13 | try:
14 | code, last_time = get_stored_code(project_path)
15 | except OSError:
16 | last_time = 0
17 |
18 | while True:
19 | pair = gen_code(last_time, interval, project_path)
20 | if pair:
21 | code = pair[0]
22 | last_time = pair[1]
23 | yield code
24 |
25 |
26 | def gen_code(last_time, interval, project_path):
27 | key = "ABCDEFGHIGKLMNOPQISTUVWXYZ0123456789"
28 |
29 | if time.time() - last_time > interval:
30 | code, last_time = "".join(random.choices(key, k=6)), time.time()
31 | with open(os.path.join(project_path, "code"), "w") as f:
32 | f.write(code)
33 | f.write("\n%d" % time.time())
34 | return code, int(last_time)
35 | return None
36 |
37 |
38 | def get_stored_code(project_path):
39 | code, last_time = open(os.path.join(project_path, "code")).read().split("\n")
40 | last_time = int(last_time)
41 | return code, last_time
42 |
--------------------------------------------------------------------------------
/src/blog/blog/welcome/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/blog/welcome/__init__.py
--------------------------------------------------------------------------------
/src/blog/blog/welcome/controller.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from apistar import App
4 | from apistar.http import QueryParam
5 |
6 | from apistellar import Controller, route, get, post, require, Session, \
7 | settings, MultiPartForm
8 |
9 |
10 | @route("/", name="welcome")
11 | class WelcomeController(Controller):
12 | """
13 | 欢迎页
14 | """
15 | @get("/")
16 | def index(self, app: App, path: QueryParam=None) -> str:
17 | """
18 | 首页
19 | :param app:
20 | :param path: 子路径
21 | :ex path:
22 | `"/article?a=3"`
23 | :param settings: 配置信息
24 | :return:
25 | ```html
26 | ...
27 | ```
28 | """
29 | return app.render_template(
30 | 'index.html',
31 | author=settings["AUTHOR"],
32 | _path=path or "",
33 | page_size=settings["PAGE_SIZE"],
34 | url_for=app.reverse_url,
35 | code_swatch=str(settings.get_bool("NEED_CODE")).lower())
36 |
37 | @post("/upload_image")
38 | @require(Session, judge=lambda x: x.get("login"))
39 | def upload(self, files: MultiPartForm):
40 | for name, file in files.items():
41 | file.save(os.path.join(
42 | settings["PROJECT_PATH"], "static/img", file.filename))
43 | return {"success": True}
44 |
45 | @post("/a/{b}/{+path}")
46 | async def test(self, b: int, path: str):
47 | print(b, path)
48 |
--------------------------------------------------------------------------------
/src/blog/cut_html.js:
--------------------------------------------------------------------------------
1 | var page = require('webpage').create();
2 | var system = require('system');
3 | if(system.args.length != 7){
4 | phantom.exit();
5 | }
6 |
7 | // page.settings.javascriptEnabled = false;
8 | //viewportSize being the actual size of the headless browser
9 | page.viewportSize = { width: 1024, height: 768 };
10 | //the clipRect is the portion of the page you are taking a screenshot of
11 | page.clipRect = {
12 | top: parseInt(system.args[3]) ,
13 | left: parseInt(system.args[4]),
14 | width: parseInt(system.args[5]),
15 | height: parseInt(system.args[6])};
16 | //the rest of the code is the same as the previous example
17 | page.open(system.args[1], function() {
18 | page.render(system.args[2]);
19 | phantom.exit();
20 | });
--------------------------------------------------------------------------------
/src/blog/db/blog:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/db/blog
--------------------------------------------------------------------------------
/src/blog/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | STATIC_DIR = "static"
3 |
4 | STATIC_URL_PATH = "/static"
5 |
6 | TEMPLATE_DIR = "templates"
7 |
8 | USERNAME = "test"
9 |
10 | PASSWORD = "12345"
11 |
12 | TIME_ZONE = 'Asia/Shanghai'
13 |
14 | AUTHOR = "夏洛之枫"
15 |
16 | PAGE_SIZE = 40
17 |
18 | CODE_EXPIRE_INTERVAL = 30*24*3600
19 |
20 | PHANTOMJS_PATH = "phantomjs"
21 |
22 | NEED_CODE = False
23 |
24 | LOCAL_VARIABLE = {"session": "apistellar.bases.entities.Session"}
25 |
--------------------------------------------------------------------------------
/src/blog/solo_app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 |
4 | from apistellar import SoloManager
5 |
6 | app_name = "blog"
7 |
8 |
9 | def run():
10 | logging.basicConfig(
11 | level=logging.DEBUG,
12 | format='%(asctime)s [%(name)s] %(levelname)s: %(message)s')
13 | SoloManager(
14 | app_name, current_dir=os.path.dirname(os.path.abspath(__file__))).start()
15 |
16 |
17 | if __name__ == "__main__":
18 | run()
19 |
--------------------------------------------------------------------------------
/src/blog/static/css/font-awesome/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/src/blog/static/editor/css/iconfont.5ce067d.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/editor/css/iconfont.5ce067d.eot
--------------------------------------------------------------------------------
/src/blog/static/editor/css/iconfont.7d3d0c4.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/editor/css/iconfont.7d3d0c4.ttf
--------------------------------------------------------------------------------
/src/blog/static/editor/css/iconfont.9fadafc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
86 |
--------------------------------------------------------------------------------
/src/blog/static/editor/css/iconfont.b300b13.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/editor/css/iconfont.b300b13.woff
--------------------------------------------------------------------------------
/src/blog/static/editor/index.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:iconfont;src:url(css/iconfont.5ce067d.eot);src:url(css/iconfont.5ce067d.eot#iefix) format('embedded-opentype'),url(css/iconfont.b300b13.woff) format('woff'),url(css/iconfont.7d3d0c4.ttf) format('truetype'),url(css/iconfont.9fadafc.svg#iconfont) format('svg')}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-webkit-text-stroke-width:.2px;-moz-osx-font-smoothing:grayscale}.icon-bold:before{content:"\EA09"}.icon-chain:before{content:"\EA36"}.icon-code:before{content:"\EA67"}.icon-compress:before{content:"\EA71"}.icon-ellipsish:before{content:"\EA95"}.icon-expand:before{content:"\EAA1"}.icon-image:before{content:"\EB26"}.icon-italic:before{content:"\EB31"}.icon-mailforward:before{content:"\EB52"}.icon-mailreply:before{content:"\EB53"}.icon-quoteleft:before{content:"\EBB8"}.icon-underline:before{content:"\EC4E"}.icon-shanchuxian2:before{content:"\E6F7"}@keyframes fadeIn{0%{opacity:0}50%{opacity:.5}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}50%{opacity:.5}to{opacity:0}}.markdown__editor .fade{animation-duration:.2s}.markdown__editor .fade.in{animation-name:fadeIn}.markdown__editor .fade.out{animation-name:fadeOut}.markdown__editor-status{position:absolute;bottom:0;padding:8px;background:rgba(0,0,0,.1);width:100%;color:#333}.markdown__editor-status.info{background:rgba(130,232,255,.12)}.markdown__editor-status.success{background:rgba(101,255,177,.12)}.markdown__editor-status.error{background:hsla(0,100%,70%,.12)}.allow{height:15px;width:15px;border-left:2px solid rgba(0,0,0,.25);border-top:2px solid rgba(0,0,0,.25)}.allow.allow-left{transform:rotate(-45deg)}.allow.allow-right{transform:rotate(135deg)}.preview-tool{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;cursor:w-resize}.preview-tool .allow-wrapper{padding:20px 8px}.preview-tool .allow-wrapper:hover{cursor:pointer;background:rgba(0,0,0,.1)}.preview-tool .allow-wrapper:hover>.allow{border-color:#000}.markdown__editor-tool{display:-ms-flexbox;display:flex;overflow-x:auto;-ms-flex-pack:justify;justify-content:space-between;width:100%;padding:0 10px;background:#f7f7f7}.markdown__editor-tool .action-group{margin-right:25px;display:-ms-flexbox;display:flex}.markdown__editor-tool .action-group:last-child{margin:0}.markdown__editor-tool .iconfont{font-size:1.5em;padding:12px 15px}.markdown__editor-tool .iconfont:hover:not(.disabled){background:#fff;cursor:pointer}.markdown__editor-tool .iconfont.disabled{color:#ccc}.markdown__editor{height:100%;width:100%;border:1px solid #f5f5f5}.markdown__editor,.markdown__editor *{box-sizing:border-box}.markdown__editor.fullscreen{top:0;left:0;position:fixed}.markdown__editor .markdown__editor-wrapper{position:relative;height:100%;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.markdown__editor .markdown__editor-content{position:relative;-ms-flex:1;flex:1;background:#fff}.markdown__editor .markdown__editor-content .content-wrapper{position:absolute;height:100%;width:100%;display:-ms-flexbox;display:flex}.markdown__editor .markdown__editor-content .content-wrapper .markdown__editor-editor{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:16px;-ms-flex:1;flex:1;border:0;outline:0;resize:none;border-right:1px solid #f5f5f5;position:relative;box-sizing:border-box;padding:10px;background:#fff}.markdown__editor .hotkey-remind{position:fixed;padding:5px 8px;color:#fff;background:rgba(0,0,0,.5)}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#333}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#183691}.markdown-body .pl-v{color:#ed6a43}.markdown-body .pl-id{color:#b52a1d}.markdown-body .pl-ii{color:#f8f8f8;background-color:#b52a1d}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#63a35c}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#1d3e81}.markdown-body .pl-mq{color:teal}.markdown-body .pl-mi{font-style:italic;color:#333}.markdown-body .pl-mb{font-weight:700;color:#333}.markdown-body .pl-md{color:#bd2c00;background-color:#ffecec}.markdown-body .pl-mi1{color:#55a532;background-color:#eaffea}.markdown-body .pl-mdr{font-weight:700;color:#795da3}.markdown-body .pl-mo{color:#1d3e81}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body strong{font-weight:inherit;font-weight:bolder}.markdown-body h1{font-size:2em;margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body code,.markdown-body kbd,.markdown-body pre{font-family:monospace,monospace;font-size:1em}.markdown-body hr{box-sizing:content-box;height:0;overflow:visible}.markdown-body input{font:inherit;margin:0;overflow:visible}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.markdown-body *{box-sizing:border-box}.markdown-body input{font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body a{color:#4078c0;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{text-decoration:underline}.markdown-body strong{font-weight:600}.markdown-body hr{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #ddd}.markdown-body hr:after,.markdown-body hr:before{display:table;content:""}.markdown-body hr:after{clear:both}.markdown-body table{border-spacing:0;border-collapse:collapse}.markdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:0;margin-bottom:0}.markdown-body h1{font-size:32px;font-weight:600}.markdown-body h2{font-size:24px;font-weight:600}.markdown-body h3{font-size:20px;font-weight:600}.markdown-body h4{font-size:16px;font-weight:600}.markdown-body h5{font-size:14px;font-weight:600}.markdown-body h6{font-size:12px;font-weight:600}.markdown-body p{margin-top:0;margin-bottom:10px}.markdown-body blockquote{margin:0}.markdown-body ol,.markdown-body ul{padding-left:0;margin-top:0;margin-bottom:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:12px}.markdown-body pre{margin-top:0;margin-bottom:0;font:12px Consolas,Liberation Mono,Menlo,Courier,monospace}.markdown-body .octicon{vertical-align:text-bottom}.markdown-body input{-webkit-font-feature-settings:"liga" 0;font-feature-settings:"liga" 0}.markdown-body:after,.markdown-body:before{display:table;content:""}.markdown-body:after{clear:both}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:none}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e7e7e7;border:0}.markdown-body blockquote{padding:0 1em;color:#777;border-left:.25em solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#000;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{font-size:2em}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body h2{font-size:1.5em}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:.875em}.markdown-body h6{font-size:.85em;color:#777}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:700}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:0;padding-top:.2em;padding-bottom:.2em;margin:0;font-size:85%;background-color:rgba(0,0,0,.04);border-radius:3px}.markdown-body code:after,.markdown-body code:before{letter-spacing:-.2em;content:"\A0"}.markdown-body pre{word-wrap:normal}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f7f7f7;border-radius:3px}.markdown-body pre code{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:after,.markdown-body pre code:before{content:normal}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:3px!important}.markdown-body .pl-2{padding-left:6px!important}.markdown-body .pl-3{padding-left:12px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:36px!important}.markdown-body .pl-6{padding-left:48px!important}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#4078c0;border:1px solid #4078c0}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas,Liberation Mono,Menlo,Courier,monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#4078c0}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}.markdown__editor-preview{min-width:20%;padding:10px;font-size:16px;overflow:auto}
--------------------------------------------------------------------------------
/src/blog/static/editor/preview.css:
--------------------------------------------------------------------------------
1 | .markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#333}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#183691}.markdown-body .pl-v{color:#ed6a43}.markdown-body .pl-id{color:#b52a1d}.markdown-body .pl-ii{color:#f8f8f8;background-color:#b52a1d}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#63a35c}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#1d3e81}.markdown-body .pl-mq{color:teal}.markdown-body .pl-mi{font-style:italic;color:#333}.markdown-body .pl-mb{font-weight:700;color:#333}.markdown-body .pl-md{color:#bd2c00;background-color:#ffecec}.markdown-body .pl-mi1{color:#55a532;background-color:#eaffea}.markdown-body .pl-mdr{font-weight:700;color:#795da3}.markdown-body .pl-mo{color:#1d3e81}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body strong{font-weight:inherit;font-weight:bolder}.markdown-body h1{font-size:2em;margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body code,.markdown-body kbd,.markdown-body pre{font-family:monospace,monospace;font-size:1em}.markdown-body hr{box-sizing:content-box;height:0;overflow:visible}.markdown-body input{font:inherit;margin:0;overflow:visible}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.markdown-body *{box-sizing:border-box}.markdown-body input{font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body a{color:#4078c0;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{text-decoration:underline}.markdown-body strong{font-weight:600}.markdown-body hr{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #ddd}.markdown-body hr:after,.markdown-body hr:before{display:table;content:""}.markdown-body hr:after{clear:both}.markdown-body table{border-spacing:0;border-collapse:collapse}.markdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:0;margin-bottom:0}.markdown-body h1{font-size:32px;font-weight:600}.markdown-body h2{font-size:24px;font-weight:600}.markdown-body h3{font-size:20px;font-weight:600}.markdown-body h4{font-size:16px;font-weight:600}.markdown-body h5{font-size:14px;font-weight:600}.markdown-body h6{font-size:12px;font-weight:600}.markdown-body p{margin-top:0;margin-bottom:10px}.markdown-body blockquote{margin:0}.markdown-body ol,.markdown-body ul{padding-left:0;margin-top:0;margin-bottom:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:12px}.markdown-body pre{margin-top:0;margin-bottom:0;font:12px Consolas,Liberation Mono,Menlo,Courier,monospace}.markdown-body .octicon{vertical-align:text-bottom}.markdown-body input{-webkit-font-feature-settings:"liga" 0;font-feature-settings:"liga" 0}.markdown-body:after,.markdown-body:before{display:table;content:""}.markdown-body:after{clear:both}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:none}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e7e7e7;border:0}.markdown-body blockquote{padding:0 1em;color:#777;border-left:.25em solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#000;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{font-size:2em}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body h2{font-size:1.5em}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:.875em}.markdown-body h6{font-size:.85em;color:#777}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:700}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:0;padding-top:.2em;padding-bottom:.2em;margin:0;font-size:85%;background-color:rgba(0,0,0,.04);border-radius:3px}.markdown-body code:after,.markdown-body code:before{letter-spacing:-.2em;content:"\A0"}.markdown-body pre{word-wrap:normal}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f7f7f7;border-radius:3px}.markdown-body pre code{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:after,.markdown-body pre code:before{content:normal}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:3px!important}.markdown-body .pl-2{padding-left:6px!important}.markdown-body .pl-3{padding-left:12px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:36px!important}.markdown-body .pl-6{padding-left:48px!important}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#4078c0;border:1px solid #4078c0}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas,Liberation Mono,Menlo,Courier,monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#4078c0}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}.markdown__editor-preview{min-width:20%;padding:10px;font-size:16px;overflow:auto}
--------------------------------------------------------------------------------
/src/blog/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/img/favicon.ico
--------------------------------------------------------------------------------
/src/blog/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/img/logo.png
--------------------------------------------------------------------------------
/src/blog/static/img/pretty/0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/img/pretty/0.jpg
--------------------------------------------------------------------------------
/src/blog/static/js/jqpage.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 | 'use strict';
3 |
4 | $.jqPaginator = function (el, options) {
5 | if(!(this instanceof $.jqPaginator)){
6 | return new $.jqPaginator(el, options);
7 | }
8 |
9 | var self = this;
10 |
11 | self.$container = $(el);
12 |
13 | self.$container.data('jqPaginator', self);
14 |
15 | self.init = function () {
16 |
17 | if (options.first || options.prev || options.next || options.last || options.page) {
18 | options = $.extend({}, {
19 | first: '',
20 | prev: '',
21 | next: '',
22 | last: '',
23 | page: ''
24 | }, options);
25 | }
26 |
27 | self.options = $.extend({}, $.jqPaginator.defaultOptions, options);
28 |
29 | self.verify();
30 |
31 | self.extendJquery();
32 |
33 | self.render();
34 |
35 | self.fireEvent(this.options.currentPage, 'init');
36 | };
37 |
38 | self.verify = function () {
39 | var opts = self.options;
40 |
41 | if (!self.isNumber(opts.totalPages)) {
42 | throw new Error('[jqPaginator] type error: totalPages');
43 | }
44 |
45 | if (!self.isNumber(opts.totalCounts)) {
46 | throw new Error('[jqPaginator] type error: totalCounts');
47 | }
48 |
49 | if (!self.isNumber(opts.pageSize)) {
50 | throw new Error('[jqPaginator] type error: pageSize');
51 | }
52 |
53 | if (!self.isNumber(opts.currentPage)) {
54 | throw new Error('[jqPaginator] type error: currentPage');
55 | }
56 |
57 | if (!self.isNumber(opts.visiblePages)) {
58 | throw new Error('[jqPaginator] type error: visiblePages');
59 | }
60 |
61 | if (!opts.totalPages && !opts.totalCounts) {
62 | throw new Error('[jqPaginator] totalCounts or totalPages is required');
63 | }
64 |
65 | if (!opts.totalPages && !opts.totalCounts) {
66 | throw new Error('[jqPaginator] totalCounts or totalPages is required');
67 | }
68 |
69 | if (!opts.totalPages && opts.totalCounts && !opts.pageSize) {
70 | throw new Error('[jqPaginator] pageSize is required');
71 | }
72 |
73 | if (opts.totalCounts && opts.pageSize) {
74 | opts.totalPages = Math.ceil(opts.totalCounts / opts.pageSize);
75 | }
76 |
77 | if (opts.currentPage < 1 || opts.currentPage > opts.totalPages) {
78 | throw new Error('[jqPaginator] currentPage is incorrect');
79 | }
80 |
81 | if (opts.totalPages < 1) {
82 | throw new Error('[jqPaginator] totalPages cannot be less currentPage');
83 | }
84 | };
85 |
86 | self.extendJquery = function () {
87 | $.fn.jqPaginatorHTML = function (s) {
88 | return s ? this.before(s).remove() : $('').append(this.eq(0).clone()).html();
89 | };
90 | };
91 |
92 | self.render = function () {
93 | self.renderHtml();
94 | self.setStatus();
95 | self.bindEvents();
96 | };
97 |
98 | self.renderHtml = function () {
99 | var html = [];
100 |
101 | var pages = self.getPages();
102 | for (var i = 0, j = pages.length; i < j; i++) {
103 | html.push(self.buildItem('page', pages[i]));
104 | }
105 |
106 | self.isEnable('prev') && html.unshift(self.buildItem('prev', self.options.currentPage - 1));
107 | self.isEnable('first') && html.unshift(self.buildItem('first', 1));
108 | self.isEnable('statistics') && html.unshift(self.buildItem('statistics'));
109 | self.isEnable('next') && html.push(self.buildItem('next', self.options.currentPage + 1));
110 | self.isEnable('last') && html.push(self.buildItem('last', self.options.totalPages));
111 |
112 | if (self.options.wrapper) {
113 | self.$container.html($(self.options.wrapper).html(html.join('')).jqPaginatorHTML());
114 | } else {
115 | self.$container.html(html.join(''));
116 | }
117 | };
118 |
119 | self.buildItem = function (type, pageData) {
120 | var html = self.options[type]
121 | .replace(/{{page}}/g, pageData)
122 | .replace(/{{totalPages}}/g, self.options.totalPages)
123 | .replace(/{{totalCounts}}/g, self.options.totalCounts);
124 |
125 | return $(html).attr({
126 | 'jp-role': type,
127 | 'jp-data': pageData
128 | }).jqPaginatorHTML();
129 | };
130 |
131 | self.setStatus = function () {
132 | var options = self.options;
133 |
134 | if (!self.isEnable('first') || options.currentPage === 1) {
135 | $('[jp-role=first]', self.$container).addClass(options.disableClass);
136 | }
137 | if (!self.isEnable('prev') || options.currentPage === 1) {
138 | $('[jp-role=prev]', self.$container).addClass(options.disableClass);
139 | }
140 | if (!self.isEnable('next') || options.currentPage >= options.totalPages) {
141 | $('[jp-role=next]', self.$container).addClass(options.disableClass);
142 | }
143 | if (!self.isEnable('last') || options.currentPage >= options.totalPages) {
144 | $('[jp-role=last]', self.$container).addClass(options.disableClass);
145 | }
146 |
147 | $('[jp-role=page]', self.$container).removeClass(options.activeClass);
148 | $('[jp-role=page][jp-data=' + options.currentPage + ']', self.$container).addClass(options.activeClass);
149 | };
150 |
151 | self.getPages = function () {
152 | var pages = [],
153 | visiblePages = self.options.visiblePages,
154 | currentPage = self.options.currentPage,
155 | totalPages = self.options.totalPages;
156 |
157 | if (visiblePages > totalPages) {
158 | visiblePages = totalPages;
159 | }
160 |
161 | var half = Math.floor(visiblePages / 2);
162 | var start = currentPage - half + 1 - visiblePages % 2;
163 | var end = currentPage + half;
164 |
165 | if (start < 1) {
166 | start = 1;
167 | end = visiblePages;
168 | }
169 | if (end > totalPages) {
170 | end = totalPages;
171 | start = 1 + totalPages - visiblePages;
172 | }
173 |
174 | var itPage = start;
175 | while (itPage <= end) {
176 | pages.push(itPage);
177 | itPage++;
178 | }
179 |
180 | return pages;
181 | };
182 |
183 | self.isNumber = function (value) {
184 | var type = typeof value;
185 | return type === 'number' || type === 'undefined';
186 | };
187 |
188 | self.isEnable = function (type) {
189 | return self.options[type] && typeof self.options[type] === 'string';
190 | };
191 |
192 | self.switchPage = function (pageIndex) {
193 | self.options.currentPage = pageIndex;
194 | self.render();
195 | };
196 |
197 | self.fireEvent = function (pageIndex, type) {
198 | return (typeof self.options.onPageChange !== 'function') || (self.options.onPageChange(pageIndex, type) !== false);
199 | };
200 |
201 | self.callMethod = function (method, options) {
202 | switch (method) {
203 | case 'option':
204 | self.options = $.extend({}, self.options, options);
205 | self.verify();
206 | self.render();
207 | break;
208 | case 'destroy':
209 | self.$container.empty();
210 | self.$container.removeData('jqPaginator');
211 | break;
212 | default :
213 | throw new Error('[jqPaginator] method "' + method + '" does not exist');
214 | }
215 |
216 | return self.$container;
217 | };
218 |
219 | self.bindEvents = function () {
220 | var opts = self.options;
221 |
222 | self.$container.off();
223 | self.$container.on('click', '[jp-role]', function (event) {
224 | var $el = $(this);
225 | if ($el.hasClass(opts.disableClass) || $el.hasClass(opts.activeClass)) {
226 | // 用来阻止a标签的默认行为
227 | event.preventDefault();
228 | return;
229 | }
230 |
231 | var pageIndex = +$el.attr('jp-data');
232 | if (self.fireEvent(pageIndex, 'change')) {
233 | self.switchPage(pageIndex);
234 | }
235 | });
236 | };
237 |
238 | self.init();
239 |
240 | return self.$container;
241 | };
242 |
243 | $.jqPaginator.defaultOptions = {
244 | wrapper: '',
245 | first: '
<<',
246 | prev: '<',
247 | next: '>',
248 | last: '>>',
249 | page: '{{page}}',
250 | totalPages: 0,
251 | totalCounts: 0,
252 | pageSize: 0,
253 | currentPage: 1,
254 | visiblePages: 7,
255 | disableClass: 'disabled',
256 | activeClass: 'active',
257 | onPageChange: null
258 | };
259 |
260 | $.fn.jqPaginator = function () {
261 | var self = this,
262 | args = Array.prototype.slice.call(arguments);
263 |
264 | if (typeof args[0] === 'string') {
265 | var $instance = $(self).data('jqPaginator');
266 | if (!$instance) {
267 | throw new Error('[jqPaginator] the element is not instantiated');
268 | } else {
269 | return $instance.callMethod(args[0], args[1]);
270 | }
271 | } else {
272 | return new $.jqPaginator(this, args[0]);
273 | }
274 | };
275 |
276 | })(jQuery);
--------------------------------------------------------------------------------
/src/blog/static/js/modernizr.js:
--------------------------------------------------------------------------------
1 | /* Modernizr 2.7.1 (Custom Build) | MIT & BSD
2 | * Build: http://modernizr.com/download/#-inlinesvg-shiv-cssclasses-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes
3 | */
4 | ;window.Modernizr=function(a,b,c){function B(a){j.cssText=a}function C(a,b){return B(m.join(a+";")+(b||""))}function D(a,b){return typeof a===b}function E(a,b){return!!~(""+a).indexOf(b)}function F(a,b){for(var d in a){var e=a[d];if(!E(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function G(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:D(f,"function")?f.bind(d||b):f}return!1}function H(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+o.join(d+" ")+d).split(" ");return D(b,"string")||D(b,"undefined")?F(e,b):(e=(a+" "+p.join(d+" ")+d).split(" "),G(e,b,c))}var d="2.7.1",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n="Webkit Moz O ms",o=n.split(" "),p=n.toLowerCase().split(" "),q={svg:"http://www.w3.org/2000/svg"},r={},s={},t={},u=[],v=u.slice,w,x=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},y=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=D(e[d],"function"),D(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),z={}.hasOwnProperty,A;!D(z,"undefined")&&!D(z.call,"undefined")?A=function(a,b){return z.call(a,b)}:A=function(a,b){return b in a&&D(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=v.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(v.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(v.call(arguments)))};return e}),r.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==q.svg};for(var I in r)A(r,I)&&(w=I.toLowerCase(),e[w]=r[I](),u.push((e[w]?"":"no-")+w));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)A(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},B(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=m,e._domPrefixes=p,e._cssomPrefixes=o,e.hasEvent=y,e.testProp=function(a){return F([a])},e.testAllProps=H,e.testStyles=x,e.prefixed=function(a,b,c){return b?H(a,b,c):H(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+u.join(" "):""),e}(this,this.document);
--------------------------------------------------------------------------------
/src/blog/static/js/react-dom-0.14.0.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ReactDOM v0.14.0
3 | *
4 | * Copyright 2013-2015, Facebook, Inc.
5 | * All rights reserved.
6 | *
7 | * This source code is licensed under the BSD-style license found in the
8 | * LICENSE file in the root directory of this source tree. An additional grant
9 | * of patent rights can be found in the PATENTS file in the same directory.
10 | *
11 | */
12 | // Based off https://github.com/ForbesLindesay/umd/blob/master/template.js
13 | ;(function(f) {
14 | // CommonJS
15 | if (typeof exports === "object" && typeof module !== "undefined") {
16 | module.exports = f(require('react'));
17 |
18 | // RequireJS
19 | } else if (typeof define === "function" && define.amd) {
20 | define(['react'], f);
21 |
22 | //
--------------------------------------------------------------------------------
/src/blog/templates/edit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 更新
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
56 |
57 |
58 |
70 |
--------------------------------------------------------------------------------
/src/blog/templates/import.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 导入
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/blog/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 登陆
4 |
5 |
6 |
7 |
20 |
21 |
--------------------------------------------------------------------------------
/src/blog/web_app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 |
4 | from uvicorn import run
5 | from apistellar import Application
6 | from whitenoise import WhiteNoise
7 |
8 | # 静态文件每次请求重新查找
9 | WhiteNoise.autorefresh = True
10 |
11 | app_name = "blog"
12 | logging.basicConfig(
13 | level=logging.DEBUG,
14 | format='%(asctime)s [%(name)s] %(levelname)s: %(message)s')
15 |
16 | app = Application(
17 | app_name, debug=False,
18 | current_dir=os.path.dirname(os.path.abspath(__file__)))
19 |
20 | if __name__ == "__main__":
21 | run(app)
22 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from apistellar import init_settings
2 |
3 | init_settings("blog.settings")
4 |
--------------------------------------------------------------------------------
/tests/factories.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from apistellar import settings
4 |
5 |
6 | def get_code():
7 | code, _ = open(os.path.join(
8 | settings["PROJECT_PATH"], "code")).read().split("\n")
9 | return code
10 |
--------------------------------------------------------------------------------
/tests/test_article/test_article.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from pytest_apistellar import prop_alias
4 | from pytest_apistellar.parser import Attr
5 |
6 | from blog.blog.article.article import Article
7 |
8 | from factories import get_code
9 |
10 | article = prop_alias("blog.blog.article.article.Article")
11 |
12 |
13 | def assert_execute(sql, args, assert_sql, assert_args, **kwargs):
14 | assert sql == assert_sql
15 | assert args == assert_args
16 |
17 |
18 | @article("code", ret_factory=get_code)
19 | @pytest.mark.env(NEED_CODE="False")
20 | @pytest.mark.env(CODE_EXPIRE_INTERVAL=30 * 24 * 3600)
21 | @pytest.mark.asyncio
22 | class TestArticle(object):
23 |
24 | pytestmark = [
25 | article("store", ret_val=Attr()),
26 | article("store.execute", callable=True)
27 | ]
28 |
29 | @pytest.mark.env(NEED_CODE="True")
30 | async def test_check_code_on_True(self):
31 | assert Article().right_code(get_code()) is True
32 |
33 | @pytest.mark.env(NEED_CODE="True")
34 | async def test_check_code_on_False(self):
35 | assert Article().right_code("111111") is False
36 |
37 | async def test_check_code_off(self):
38 | assert Article().right_code("111111") is True
39 |
40 | @article("store.description", ret_val=[("id", )])
41 | @article("store.fetchone", ret_val=("111", ), callable=True)
42 | async def test_load_with_article(self):
43 | article = await Article.load()
44 | assert article["id"] == "111"
45 |
46 | @article("store.execute", ret_factory=assert_execute,
47 | assert_sql=f"INSERT INTO {Article.TABLE} "
48 | f"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
49 | assert_args=("2017010101010", "ccc", "aaa,bbb", "dddd", "夏洛之枫",
50 | "aaa", True, 1539140711.0, 1539140711.0, False))
51 | async def test_save(self):
52 | data = {
53 | "id": "2017010101010",
54 | "description": "ccc",
55 | "tags": "aaa,bbb",
56 | "article": "dddd",
57 | "author": "夏洛之枫",
58 | "title": "aaa",
59 | "feature": "1",
60 | "created_at": "2018-10-10 11:11:11",
61 | "updated_at": "2018-10-10 11:11:11",
62 | "show": "0"
63 | }
64 | article = Article(data)
65 | await article.save()
66 |
67 | @article("store.execute", ret_factory=assert_execute,
68 | assert_sql=f"DELETE FROM {Article.TABLE} WHERE show=1 and id=?;",
69 | assert_args=("2017010101010", ))
70 | async def test_remove(self):
71 | article = Article(id="2017010101010")
72 | await article.remove()
73 |
74 | @article("_build_update_sql", ret_val=("test_sql", ["1"]))
75 | @article("store.execute", ret_factory=assert_execute,
76 | assert_sql="test_sql",
77 | assert_args=["1"])
78 | async def test_update(self):
79 | data = {
80 | "id": "2017010101010",
81 | "description": "ccc",
82 | "show": "0"
83 | }
84 | article = Article(data)
85 | await article.update()
86 |
87 | @article("store.fetchone", callable=True)
88 | async def test_load_without_article(self):
89 | assert "id" not in await Article.load()
90 |
91 | @article("load_list",
92 | ret_val=[Article(tags="python,abc"), Article(tags="abc")])
93 | async def test_get_total_tags(self):
94 | total, tags = await Article.get_total_tags()
95 | assert total == 2
96 | assert tags["python"] == 1
97 | assert tags["abc"] == 2
98 |
99 | @article("_build_select_sql", ret_val=(
100 | ["a", "b"], "SELECT * FROM articles WHERE 1=1 AND a=? AND b=?"))
101 | @article("store.execute", ret_factory=assert_execute,
102 | assert_sql="SELECT * FROM articles WHERE 1=1 AND a=? AND b=?",
103 | assert_args=["a", "b"])
104 | @article("store.fetchall", ret_val=[("abc", "python")], callable=True)
105 | @article("store.description", ret_val=[("title", ), ("tags", )])
106 | async def test_load_list(self):
107 | article_list = await Article.load_list()
108 | assert article_list[0].title == "abc"
109 | assert article_list[0].tags == "python"
110 |
111 | @article("load_list", ret_factory=lambda sub=None, **kwargs: sub)
112 | async def test_search(self):
113 | assert await Article.search("abc", 0, 10, fulltext=True) == \
114 | " AND (article LIKE ? OR tags LIKE ?)"
115 |
116 | async def test_fuzzy_search_sub_sql_without_field(self):
117 | assert Article._fuzzy_search_sub_sql("", False) == (None, None)
118 |
119 | async def test_fuzzy_search_sub_sql_with_field_fulltext(self):
120 | sub, vals = Article._fuzzy_search_sub_sql("abc", True)
121 | assert sub == " AND (article LIKE ? OR tags LIKE ?)"
122 | assert vals == ['%abc%', '%abc%']
123 |
124 | async def test_fuzzy_search_sub_sql_with_field_fulltext_false(self):
125 | sub, vals = Article._fuzzy_search_sub_sql("abc", False)
126 | assert sub == " AND tags LIKE ?"
127 | assert vals == ['%abc%']
128 |
129 | async def test_build_select_sql_with_no_args(self):
130 | vals, sql = Article._build_select_sql()
131 | assert vals == list()
132 | assert sql == f"SELECT * FROM {Article.TABLE} WHERE 1=1;"
133 |
134 | async def test_build_select_sql_with_vals_and_sub(self):
135 | vals, sql = Article._build_select_sql(vals=["name"], sub=" AND title=?")
136 | assert vals == ["name"]
137 | assert sql == f"SELECT * FROM {Article.TABLE} WHERE 1=1 AND title=?;"
138 |
139 | async def test_build_select_sql_with_kwargs_of_in_opt(self):
140 | vals, sql = Article._build_select_sql({"id": ["1", "2"], "title": "a"})
141 | assert "1" in vals
142 | assert "2" in vals
143 | assert "a" in vals
144 | assert "id IN (?, ?)" in sql
145 | assert "title=?" in sql
146 |
147 | async def test_build_select_sql_with_order_fields_and_projection(self):
148 | vals, sql = Article._build_select_sql(
149 | projection=["tags", "id", "title"],
150 | order_fields=[("id", "desc"), ("title", "asc")])
151 | assert vals == []
152 | assert "id desc" in sql
153 | assert "title asc" in sql
154 | assert "tags, id, title" in sql
155 |
156 | async def test_build_select_sql_with_size_and_from(self):
157 | vals, sql = Article._build_select_sql(size=10, _from=0)
158 | assert vals == []
159 | assert "limit 10 offset 0" in sql
160 |
161 | async def test_build_select_sql_with_size_and_without_from(self):
162 | with pytest.raises(AssertionError):
163 | Article._build_select_sql(size=10)
164 |
165 | async def test_build_update_sql(self):
166 | data = {
167 | "id": "2017010101010",
168 | "tags": "aaa,bbb",
169 | "feature": "1"
170 | }
171 | article = Article(data)
172 | sql, args = Article._build_update_sql(article)
173 | assert sql == f"UPDATE {Article.TABLE} SET tags=?, feature=?, " \
174 | f"updated_at=? WHERE id=?;"
175 | assert args[0] == "aaa,bbb"
176 | assert args[1] is True
177 | assert args[3] == "2017010101010"
178 |
--------------------------------------------------------------------------------
/tests/test_article/test_article_exporter.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from pytest_apistellar import prop_alias
4 | from blog.blog.article.article import Article
5 | from blog.blog.article.article_exporter import ArticleExporter
6 |
7 | article = prop_alias("blog.blog.article.article.Article")
8 |
9 |
10 | @pytest.mark.asyncio
11 | class TestArticleExporter(object):
12 | async def test__choice_function(self):
13 | article = Article(id="me")
14 | article_exporter = ArticleExporter(article, "", "")
15 | assert article_exporter._choice_function() == article_exporter.export_me
16 | article = Article(id="xxx")
17 | article_exporter = ArticleExporter(article, "", "")
18 | assert article_exporter._choice_function() == article_exporter.export_other
19 |
20 | @article("right_code", ret_val=False)
21 | async def test_export_me_code_not_right(self):
22 | article = Article(id="me")
23 | article_exporter = ArticleExporter(article, "", "")
24 | with pytest.raises(AssertionError):
25 | await article_exporter.export_me()
26 |
27 | @article("right_code", ret_val=True)
28 | async def test_export_me_code_right(self):
29 | article = Article(id="me", tags=["a", "b"],
30 | title="b", description="11", author="ma")
31 | article.format()
32 | article_exporter = ArticleExporter(article, "", "")
33 | article_file = await article_exporter.export_me()
34 | assert article_file.filename == "b.pdf"
35 | assert len(article_file.buffer) > 10
36 |
37 | @article("right_code", ret_val=True)
38 | async def test__replace_url(self):
39 | article_exporter = ArticleExporter("", "", "http://www.baidu.com/abc")
40 | html = '
' \
41 | '
'
42 | html = article_exporter._replace_url(html)
43 | assert html.count("http://www.baidu.com/cut"
44 | "?width=60&height=20&url=http://img.shields.io/aaa")
45 | assert html.count("http://www.amazon.com/01.jpg")
46 |
47 | @article("right_code", ret_val=False)
48 | async def test_export_other(self):
49 | article = Article(id="1234", tags=["a", "b"], article="1111",
50 | title="b", description="11", author="ma")
51 | article_exporter = ArticleExporter(article, "", "")
52 | article_file = await article_exporter.export_other()
53 | assert article_file.filename == "b.md"
54 | assert article_file.buffer.count(b"comment") == 4
--------------------------------------------------------------------------------
/tests/test_article/test_service.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 |
4 | from io import BytesIO
5 | from zipfile import ZipFile
6 | from apistellar import settings
7 | from collections import defaultdict
8 | from pytest_apistellar import prop_alias
9 | from toolkit.settings import SettingsLoader
10 |
11 | from blog.blog.article.service import ArticleService, Article
12 | from blog.blog.article.article_exporter import ArticleFile
13 | arti_ser = prop_alias("blog.blog.article.service.ArticleService")
14 | article = prop_alias("blog.blog.article.article.Article")
15 |
16 |
17 | @arti_ser("settings", ret_val=SettingsLoader().load("blog.settings"))
18 | @pytest.mark.asyncio
19 | class TestService(object):
20 |
21 | @article("load", ret_val=Article(id="20181010101010",
22 | article=""))
23 | async def test_get(self):
24 | article = await ArticleService().get("20181010101010")
25 | assert article["first_img"] == "http://www.baidu.com/"
26 |
27 | @pytest.mark.prop(
28 | "blog.blog.article.article_exporter.ArticleExporter.export",
29 | ret_val=ArticleFile("test.pdf", b"aaaaa"))
30 | async def test_export(self):
31 | file_resp = await ArticleService().export([1], "", "")
32 | zip_file = BytesIO(file_resp.content)
33 | zf = ZipFile(zip_file)
34 | for file in zf.infolist():
35 | assert file.filename == "test.pdf"
36 | assert file.file_size == 5
37 |
38 | @article("update")
39 | async def test_modify(self):
40 | article = Article(article="aaa
")
41 | await ArticleService().modify(article, "http://www.baidu.com/")
42 | assert article.article == "[comment]: (![]" \
43 | "(http://www.baidu.com/))\n# aaa\n\n"
44 |
45 | @article("update")
46 | async def test_update(self):
47 | article = Article(id="20180101010101")
48 | await ArticleService().update(article)
49 | assert article.to_dict() == {"id": "20180101010101"}
50 |
51 | @article("save")
52 | async def test_upload_without_title(self, join_root_dir):
53 | f = open(join_root_dir("test_data/一键生成API文档.md"), "rb")
54 | f.filename = os.path.basename(f.name)
55 | article = Article(title="", article=f)
56 | await ArticleService().upload(article)
57 | assert article.title == "一键生成API文档"
58 | f.seek(0)
59 | assert article.article == f.read().decode()
60 |
61 | @article("save")
62 | async def test_upload_with_title(self, join_root_dir):
63 | f = open(join_root_dir("test_data/一键生成API文档.md"), "rb")
64 | article = Article(title="abc", article=f)
65 | await ArticleService().upload(article)
66 | assert article.title == "abc"
67 |
68 | @article("remove")
69 | async def test_remove(self):
70 | article = Article(id="20180101010101")
71 | await ArticleService().delete(article)
72 | assert article.to_dict() == {"id": "20180101010101"}
73 |
74 | @article("load", ret_val=Article(id="me",
75 | article=""))
76 | async def test_about_with_article(self):
77 | article = await ArticleService().about("me")
78 | assert article["first_img"] == "http://www.baidu.com/"
79 | assert article["article"] == '
'
80 |
81 | @article("load", ret_val=Article())
82 | @article("save")
83 | @pytest.mark.env(AUTHOR="test")
84 | async def test_about_without_article(self):
85 | id = "me"
86 | article = await ArticleService().about(id)
87 | assert article["first_img"] == ""
88 | assert article["author"] == "test"
89 | assert article["tags"] == id
90 | assert article["description"] == id
91 | assert article["feature"] is False
92 | assert article["title"] == id
93 | assert article["show"] is False
94 | assert article["article"] == "" + id + "
"
95 | assert "updated_at" in article
96 | assert "created_at" in article
97 |
98 | @article("search", ret_factory=
99 | lambda *args, **kwargs: [Article(article="")])
100 | @article("get_total_tags", ret_val=(1, defaultdict(a=1, b=2)))
101 | async def test_show(self):
102 | rs = await ArticleService().show("", 0, 10, False)
103 | article = dict()
104 | article["first_img"] = "http://www.baidu.com/"
105 | assert rs["count"] == 1
106 | assert rs["tags"] == [("b", 2), ("a", 1)]
107 | assert rs["feature_articles"][0] == article
108 | assert rs["articles"][0] == article
109 |
110 | @pytest.mark.prop("asyncio.unix_events."
111 | "_UnixSelectorEventLoop.run_in_executor", asyncable=True)
112 | async def test_cut(self):
113 | save_name = await ArticleService().cut(
114 | "http://www.baidu.com/", 0, 0, 1024, 768)
115 | assert save_name == os.path.join(
116 | settings["PROJECT_PATH"], "static/temp/1169ee22f8.png")
117 |
--------------------------------------------------------------------------------
/tests/test_data/一键生成API文档.md:
--------------------------------------------------------------------------------
1 | # abcde
--------------------------------------------------------------------------------
/tests/test_import/test_import.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from apistellar import settings
4 | from pytest_apistellar import prop_alias
5 |
6 | from blog.blog.import_ import Import, Article
7 |
8 | article = prop_alias("blog.blog.article.article.Article")
9 |
10 |
11 | @pytest.mark.asyncio
12 | class TestImport(object):
13 |
14 | async def test_retrieve_success(self):
15 | article = ["aaaa", "[comment]: (abcde)"]
16 | val = Import.retrieve("title", article)
17 | assert val == "abcde"
18 | assert len(article) == 1
19 | assert article[0] == "aaaa"
20 |
21 | async def test_retrieve_failed(self):
22 | article = ["aaaa"]
23 | val = Import.retrieve("title", article)
24 | assert val == ""
25 |
26 | @article("load", ret_val=Article())
27 | @article("save")
28 | async def test_insert(self, join_root_dir):
29 | article = await Import.insert(
30 | join_root_dir("test_data/一键生成API文档.md"))
31 | assert article.title == "一键生成API文档"
32 | assert article.tags == []
33 | assert article.author == settings["AUTHOR"]
34 |
35 | @article("load", ret_val=Article(title="已存在的文件", tags=["1"]))
36 | @article("save")
37 | async def test_insert_exists(self, join_root_dir):
38 | article = await Import.insert(
39 | join_root_dir("test_data/一键生成API文档.md"))
40 | assert article.title == "已存在的文件"
41 | assert article.tags == ["1"]
42 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import time
2 | import pytest
3 |
4 | from blog.blog.utils import code_generator, get_stored_code, gen_code
5 |
6 | from factories import get_code
7 |
8 |
9 | def test_gen_code(join_root_dir):
10 | code, last_time = gen_code(0, 3600, join_root_dir("test_data/"))
11 | assert len(code) == 6
12 | assert last_time - int(time.time()) < 1
13 |
14 |
15 | def test_gen_code_not_expired(join_root_dir):
16 | rs = gen_code(time.time(), 3600, join_root_dir("test_data/"))
17 | assert rs is None
18 |
19 |
20 | def test_get_stored_code(join_root_dir):
21 | code, last_time = get_stored_code(join_root_dir("test_data/"))
22 | assert len(code) == 6
23 | assert isinstance(last_time, int)
24 |
25 |
26 | @pytest.mark.prop("blog.blog.utils.get_stored_code", ret_val=("ABCDEF", 111))
27 | @pytest.mark.prop("blog.blog.utils.gen_code")
28 | def test_code_generator_stored_not_expired():
29 | gen = code_generator(10)
30 | assert next(gen) == "ABCDEF"
31 |
32 |
33 | # 通过打开一个假文件来触发一次OSError
34 | @pytest.mark.prop("blog.blog.utils.get_stored_code",
35 | ret_factory=lambda *args, **kwargs: open("/tmp/abc/abc/abc"))
36 | def test_code_generator_not_stored_expired():
37 | gen = code_generator(10)
38 | assert next(gen) == get_code()
39 |
--------------------------------------------------------------------------------
/tools/auto_commit.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | import re
4 | import time
5 |
6 | from argparse import ArgumentParser
7 | regex = re.compile("(modified:)|(Untracked files)")
8 |
9 |
10 | def main(paths):
11 | for path in paths:
12 | os.chdir(path)
13 | process = os.popen("/usr/bin/git status")
14 | buffer = process.read()
15 | if regex.search(buffer):
16 | os.system("/usr/bin/git add .")
17 | os.system("/usr/bin/git commit -m 'Save: Data auto save'")
18 | os.system("/usr/bin/git push")
19 |
20 |
21 | if __name__ == "__main__":
22 | parser = ArgumentParser()
23 | parser.add_argument("path", nargs="+", help="Which path to run save check. ")
24 | parser.add_argument("-i", "--interval", type=int, help="Check interval. ")
25 | args = parser.parse_args()
26 | while True:
27 | time.sleep(args.interval)
28 | print("Save check")
29 | main(args.path)
30 |
--------------------------------------------------------------------------------
/tools/build.sh:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/tools/build.sh
--------------------------------------------------------------------------------
/tools/change_profile.py:
--------------------------------------------------------------------------------
1 | #!/root/.pyenv/shims/python
2 | # -*- coding:utf-8 -*-
3 | import re
4 | import os
5 | import sys
6 |
7 | conf = open("/etc/nginx/conf.d/blog.conf", "r")
8 |
9 | data = conf.read()
10 |
11 | data = re.sub(r"(?<=//)(.*?)(?=\:3031;)", sys.argv[1], data)
12 |
13 | conf.close()
14 |
15 | conf = open("/etc/nginx/conf.d/blog.conf", "w")
16 |
17 | conf.write(data)
18 |
19 | conf.close()
20 |
21 | reboot = open("/etc/remote_reboot.py", "r")
22 |
23 | data = reboot.read()
24 |
25 | data = re.sub("(--host )(.*)( --port 22)", "\g<1>%s\g<3>"%sys.argv[1], data)
26 |
27 | reboot.close()
28 |
29 | reboot = open("/etc/remote_reboot.py", "w")
30 |
31 | reboot.write(data)
32 |
33 | reboot.close()
34 |
35 | os.system("systemctl restart nginx")
36 |
--------------------------------------------------------------------------------
/tools/cut.js:
--------------------------------------------------------------------------------
1 | const CDP = require('chrome-remote-interface');
2 | const argv = require('minimist')(process.argv.slice(2));
3 | const file = require('fs');
4 |
5 | // CLI Args
6 | const url = argv.url || 'https://www.google.com';
7 | const format = argv.format === 'jpeg' ? 'jpeg' : 'png';
8 | const viewportWidth = argv.viewportWidth || 1440;
9 | const viewportHeight = argv.viewportHeight || 900;
10 | const delay = argv.delay || 0;
11 | const userAgent = argv.userAgent;
12 | const fullPage = argv.full;
13 |
14 | // Start the Chrome Debugging Protocol
15 | CDP(async function(client) {
16 | // Extract used DevTools domains.
17 | const {DOM, Emulation, Network, Page, Runtime} = client;
18 |
19 | // Enable events on domains we are interested in.
20 | await Page.enable();
21 | await DOM.enable();
22 | await Network.enable();
23 |
24 | // If user agent override was specified, pass to Network domain
25 | if (userAgent) {
26 | await Network.setUserAgentOverride({userAgent});
27 | }
28 |
29 | // Set up viewport resolution, etc.
30 | const deviceMetrics = {
31 | width: viewportWidth,
32 | height: viewportHeight,
33 | deviceScaleFactor: 0,
34 | mobile: false,
35 | fitWindow: false,
36 | };
37 | await Emulation.setDeviceMetricsOverride(deviceMetrics);
38 | await Emulation.setVisibleSize({width: viewportWidth, height: viewportHeight});
39 |
40 | // Navigate to target page
41 | await Page.navigate({url});
42 |
43 | // Wait for page load event to take screenshot
44 | Page.loadEventFired(async () => {
45 | // If the `full` CLI option was passed, we need to measure the height of
46 | // the rendered page and use Emulation.setVisibleSize
47 | if (fullPage) {
48 | const {root: {nodeId: documentNodeId}} = await DOM.getDocument();
49 | const {nodeId: bodyNodeId} = await DOM.querySelector({
50 | selector: 'body',
51 | nodeId: documentNodeId,
52 | });
53 | const {model: {height}} = await DOM.getBoxModel({nodeId: bodyNodeId});
54 |
55 | await Emulation.setVisibleSize({width: viewportWidth, height: height});
56 | // This forceViewport call ensures that content outside the viewport is
57 | // rendered, otherwise it shows up as grey. Possibly a bug?
58 | await Emulation.forceViewport({x: 0, y: 0, scale: 1});
59 | }
60 |
61 | setTimeout(async function() {
62 | const screenshot = await Page.captureScreenshot({format});
63 | const buffer = new Buffer(screenshot.data, 'base64');
64 | file.writeFile('output.png', buffer, 'base64', function(err) {
65 | if (err) {
66 | console.error(err);
67 | } else {
68 | console.log('Screenshot saved');
69 | }
70 | client.close();
71 | });
72 | }, delay);
73 | });
74 | }).on('error', err => {
75 | console.error('Cannot connect to browser:', err);
76 | });
77 |
--------------------------------------------------------------------------------
/tools/ip_change.py:
--------------------------------------------------------------------------------
1 | #!/home/pi/.pyenv/shims/python
2 | # -*- coding:utf-8 -*-
3 | import sys
4 | import time
5 | import socket
6 | import requests
7 | import paramiko
8 |
9 | current_ip = None
10 | user = "root"
11 | password = sys.argv[2]
12 | port = 28553
13 | host = sys.argv[1]
14 |
15 |
16 | def change(ip):
17 | ssh = paramiko.SSHClient()
18 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
19 | ssh.connect(host, port, user, password)
20 | stdin, stdout, stderr = ssh.exec_command("./change_profile.py %s"%ip)
21 | error = stderr.read()
22 | out = stdout.read()
23 | print(error, out)
24 | ssh.close()
25 | return error == b""
26 |
27 |
28 | def getip():
29 | # sock = socket.create_connection(('ns1.dnspod.net', 6666))
30 | # ip = sock.recv(16)
31 | # sock.close()
32 | # return ip.decode("utf-8")
33 | return requests.get("http://ip.42.pl/raw").text
34 |
35 | if __name__ == '__main__':
36 | while True:
37 | try:
38 | ip = getip()
39 | print(ip)
40 | if current_ip != ip:
41 | if change(ip):
42 | current_ip = ip
43 | except Exception as e:
44 | print(e)
45 | time.sleep(30)
46 | sys.stdout.flush()
47 |
--------------------------------------------------------------------------------
/tools/stop.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | import re
4 |
5 | from argparse import ArgumentParser
6 |
7 |
8 | def kill(pid, signal):
9 | try:
10 | os.kill(pid, signal)
11 | except ProcessLookupError:
12 | pass
13 |
14 |
15 | def main(cmd, signal):
16 | current_pid = os.getpid()
17 | pids = list()
18 | ppids = list()
19 | buffer_lines = os.popen("ps -ef|grep '%s'"%cmd).readlines()
20 | for line in buffer_lines:
21 | if line.strip():
22 | ele = re.findall("(\S+)", line.strip())
23 | if int(ele[1]) != current_pid:
24 | pids.append(int(ele[1]))
25 | ppids.append(int(ele[2]))
26 | if len(pids) > 3:
27 | for index, pid in enumerate(pids):
28 | if pid in ppids:
29 | kill(pid, signal)
30 | elif ppids[index] in pids:
31 | kill(pid, signal)
32 | if len(pids) < 3:
33 | print("No such process")
34 | else:
35 | for pid in pids:
36 | kill(pid, signal)
37 |
38 |
39 | if __name__ == "__main__":
40 | parse = ArgumentParser()
41 | parse.add_argument("-c", "--command", required=True, help="check command to stop. ")
42 | parse.add_argument("-s", "--signal", type=int, help="signal to send", default=15)
43 | args = parse.parse_args()
44 | main(args.command, args.signal)
45 |
--------------------------------------------------------------------------------