0%

Hexo NexT 添加多级相册功能

上一篇关于Hexo的文章 之后,其中关于相册的功能一直没太能完全实现,暂时采用的是 HEXO博客搭建相册 的解决方案,但是没有子相册的功能。时隔将近一个月之后,自己在 Hexo NexT 主题下自己实现了自己想要的功能,大体能够满足我的需求,也许以后还会再改。这篇文章仅仅当做自己的记录,期待能够给其他人带来一些参考。

相册期待实现效果

对于相册,在自己 基于Hexo NexT搭建 的博客上期待实现与豆瓣相册类似的效果,具体如下

  • 主界面
    • 分类相册
    • 自定义相册名
    • 自定义封面
  • 分类相册界面
    • 三等分列
    • 点击看大图
    • 本地图片源/图床外链均可
    • 与文章插图格式保持统一
  • 其他
    • 每张图片都可以有对应的文字描述
    • 游客可以为图片添加评论
    • 相册里面也可以插入视频

目前可查到的方案

自己也到Hexo NexT的github社区问过大家的意见。确实作为相册这个功能,每个人都有自己独特的需求,如果能够开发成专门的插件似乎是一个更好的选择。(也许以后可以再填一填坑?现在确实还没太有时间来搞这个)

我的实现方案

我的方案基本上是 hexo博客添加一级分类相册功能HEXO博客搭建相册 两篇文章方法的综合体。具体如下

一级分类相册 Album

一级分类相册需要有自己专门的页面,我们可以自己定制Album的页面,然后Hexo就会基于模板渲染出对应的html页面。这里只是根据配置,在Album页面加入了各个gallery的封面,相册名和相册描述。根据需要,你也可以添加更新时间,相册的图片数量等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{% extends '_layout.swig' %}
{% import '_macro/sidebar.swig' as sidebar_template with context %}

{% block content %}
<div class="posts-expand">
<div class="post-block" lang="{{ page.lang or page.language or config.language }}">
{% include '_partials/page/page-header.swig' %}
<div class="post-body{%- if page.direction and page.direction.toLowerCase() === 'rtl' %} rtl{%- endif %}">
{% if config.album %}
<div class="album-wrapper row">
{% for gallery in config.album %}
<div class="gallery-box">
<a href="./{{ gallery.name }}" class="gallery-item">
<div class="gallery-cover-box" style="background-image: url({{ gallery.imageBed }}/{{ gallery.name }}/artwork/{{ gallery.cover }});">
</div>
<p class="gallery-name">
{{ gallery.name }}
</p>
</a>
<p class="gallery-description">
{{ gallery.description }}
<p>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% include '_partials/page/breadcrumb.swig' %}
</div>
</div>
{% endblock %}

{% block sidebar %}
{{ sidebar_template.render(true) }}
{% endblock %}

在渲染的时候,我们需要用到站点配置文件中的一些参数。比如到底有多少相册,每个相册的名字是什么,封面的图片是什么,相册的描述怎样。这里需要在站点配置文件中添加如下:

1
2
3
4
5
6
7
8
9
10
11
album:
imageBed: https://xxx.s3-ap-southeast-1.amazonaws.com/album
gallery:
- name: 'Period'
cover: '2019-09-12-flash.jpg'
description: '时间'
created: '2018-07-23'
- name: 'Cosmos'
cover: '2019-11-29-yuki.jpg'
description: '秩序'
created: '2018-07-23'

对于Album页面,这里只是生成了html页面,你可以根据css来定制对应的样式。这里我的css数据放在了blog/souce/_data/styles.styl中,这里放置了我所有的自定义的样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.gallery-wrapper{
padding-top: 30px;
}
.gallery-wrapper .gallery-box{
padding: 5px !important;
}

.gallery-wrapper .gallery-item {
display: block;
overflow: hidden;
background-color: #fff;
padding: 5px;
padding-bottom: 0;
position: relative;
-moz-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.22);
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.22);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.22);
}

.gallery-cover-box{
width: 100%;
padding-top: 60%;
text-align: center;
overflow: hidden;
position: relative;
background: center center no-repeat;
-webkit-background-size: cover;
background-size: cover;
}

.gallery-cover-box .gallery-cover-img {
display: inline-block;
width: 100%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.gallery-item .gallery-name{
font-size: 14px;
line-height: 24px;
text-align: center;
color: #666;
margin: 0;
}

有了这些模板之后,只需要在blog/souce目录下创建album目录,在目录下创建对应的index.md即可

1
2
3
4
5
---
title: 相册
layout: "album"
comments: false
---

可以看到,在Album页面,每一个二级相册Gallery都可以通过点击封面作为一个链接访问。访问请求是/album/<gallery-name>

Gallery页面描述文件

因此,我们在Album目录下,为每一个Gallery创建子目录,比如这里的PeriodCosmos 。类似的我们为gallery创建了一个渲染模板,在Period目录下,我们只需要有对应的 index.md 和 描述gallery的JSON文件即可。

index.md
1
2
3
4
5
6
---
layout: gallery
title: Period
galleryName: Period
comments: false
---

下面是描述相册的JSON文件,你可以通过Python脚本自动生成和更新。

data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"name": "Period",
"cover": "2019-09-12_flash.jpg",
"description": "时光",
"created": "2019-07-23",
"imageBed": "https://xxx.s3-ap-southeast-1.amazonaws.com/album",
"items": [
{
"date": "2019-11",
"year": 2019,
"month": 11,
"images": [
{
"name": "2019-11-10_博雅塔.jpg",
"caption": "晚秋的博雅塔",
"type": "image",
"date": "2019-11-10",
"width": 6400,
"height": 4000
},
{
"name": "2019-11-10_狗狗.jpg",
"caption": "二体旁边晒太阳的狗狗",
"type": "image",
"date": "2019-11-10",
"width": 6400,
"height": 4000
}
]
},
{
"date": "2019-09",
"year": 2019,
"month": 9,
"images": [
{
"name": "2019-09-12_车流.jpg",
"caption": "第一次拍车流",
"type": "image",
"date": "2019-09-12",
"width": 6400,
"height": 4000
}
]
}
]
}

Gallery页面样式

接下来是layout下面的gallery.swig文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{% extends '_layout.swig' %}
{% import '_macro/sidebar.swig' as sidebar_template with context %}

{% block title %}{{ page.title }} | {{ title }}{% endblock %}

{% block content %}

<div class="posts-expand">
<div class="post-block" lang="{{ page.lang or page.language or config.language }}">
{% include '_partials/page/page-header.swig' %}
<div class="post-body{%- if page.direction and page.direction.toLowerCase() === 'rtl' %} rtl{%- endif %}">
<link rel="stylesheet" href="/lib/album/gallery.css">
<link rel="stylesheet" href="/lib/album/photoswipe.css">
<link rel="stylesheet" href="/lib/album/default-skin/default-skin.css">

<div class="gallery-description">
<p id="gallery-description">这里是相册描述</p>
</div>

<div class="instagram itemscope">
<a href="http://houmin.cc" target="_blank" class="open-ins">图片正在加载中…</a>
</div>

<script>
(function() {
var loadScript = function(path) {
var $script = document.createElement('script')
document.getElementsByTagName('body')[0].appendChild($script)
$script.setAttribute('src', path)
}
setTimeout(function() {
loadScript('/lib/album/gallery.js')
}, 0)
})()
</script>
</div>
{% include '_partials/page/breadcrumb.swig' %}
</div>
</div>
{% endblock %}

{% block sidebar %}
{{ sidebar_template.render(true) }}
{% endblock %}

其实到这一步已经和 HEXO博客搭建相册 里面说的很像了。只是我把这些给模板化了,同时修改了JSON文件的数据格式。

Gallery页面JS脚本

然后接下来就是修改gallery.js了,不知道写前端的人代码风格怎么样,原来的gallery.js(也就是之前教程里面的ins.js)代码让我看起来感觉不是很清晰。原来相关代码比较长,也不是原生手写的代码,而是通过webpack自动打包生成的代码。因此学习了一下前端技术中的webpack,下面具体梳理一下相关脚本的逻辑思路。

webpack打包环境

关于webpack,可以参考教程

主机系统上安装的webpack版本是4.41.2,关于文件组织结构如下

1
2
3
4
5
6
7
8
9
10
11
╭─ houmin@cosmos   ~/blog/album_script     master
╰─ tree
.
├── dist
│   └── gallery.js
├── src
│   ├── gallery.js
│   └── photoswipe.js
└── webpack.config.js

2 directories, 4 files

其中,webpack.config.js代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const path = require('path');

module.exports = {
// mode: "development",
mode: "production",
entry: "./src/gallery.js",
output: {
path: path.resolve(__dirname, 'dist'),
filename: "gallery.js"
},
optimization: {
minimize: false
}
};

可以看到,webpack的大包入口文件是gallery.js

gallery文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
"use strict";

require('lazyloadjs');
var photoswipe = require("./photoswipe.js");
var view = _interopRequireDefault(photoswipe.viewer);
var galleryPath = window.location.pathname;
var galleryName = galleryPath.split("/")[2];
var dataUrl = '/album/' + galleryName + '/data';
var dataJSON;

function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}

var render = function render(response) {
var meta = response["meta"];
var data = response["data"];
var imageBed = meta["imageBed"];
var description = meta["description"];
var ulTmpl = "";
for (var i in data) {
var liTmpl = "";
var item = data[i];
var date = item["date"];
var year = date.substring(0, 4);
var month = date.substring(5, 7);
for (var j in item["images"]) {
var image = item["images"][j];
var thumbnail = imageBed + '/' + galleryName + "/thumbnail/" + image.name;
var artwork = imageBed + '/' + galleryName + "/artwork/" + image.name;
var caption = image.caption;
var width = image.width;
var height = image.height;

liTmpl += `<figure class="thumb" itemprop="associatedMedia" itemscope="" itemtype="http://schema.org/ImageObject">
<a href="${artwork}" itemprop="contentUrl" data-size="${width}x${height}">
<img class="reward-img" data-src="${thumbnail}" src="/lib/album/assets/empty.png" itemprop="thumbnail" onload="lzld(this)">
</a>
<figcaption style="display:none" itemprop="caption description">${caption}</figcaption>
</figure>`;
}
ulTmpl += `<section class="archives album"><h1 class="timeline">${year}${month}月</h1>
<ul class="img-box-ul">${liTmpl}</ul>
</section>`;
}
document.querySelector('.instagram').innerHTML = `<div class=${photoswipe.galleryClass} itemscope="" itemtype="http://schema.org/ImageGallery">${ulTmpl}</div>`;
document.querySelector('#gallery-description').innerHTML = description;

// createVideoIcon();
view.default.init();
};

function loadData(render) {
if (!dataJSON) {
var xhr = new XMLHttpRequest();
xhr.open('GET', dataUrl + '?t=' + new Date(), true);
xhr.onload = function() {
if (this.status >= 200 && this.status < 300) {
dataJSON = JSON.parse(this.response);
render(dataJSON);
} else {
console.error(this.statusText);
}
};
xhr.onerror = function() {
console.error(this.statusText);
};
xhr.send();
} else {
render(dataJSON);
}
}

var Gallery = {
init: function init() {
loadData(function(data) {
render(data);
});
}
};

Gallery.init();

可以看到,这里的gallery.js依赖了 lazyloadjsphotoswipe.js, 整个文件的思路非常直接

  • 初始化Gallery对象,它会在初始化的时候通过XHR请求打开对应Gallery的JSON描述文件
  • 获取JSON文件的响应后,通过render函数来解析JSON数据,从而拼接处对应的HTML文本
  • 这里具体的相册名是通过解析访问的链接名得到的 window.location.pathname
  • 为了能够使用 photoswipe 插件,我们需要在render函数中讲 view 对象初始化

关于 lazyloadjs 插件的使用,可以参考 其GitHub说明文档

photoswipe文件

这里的 photoswipe.js 主要参考了photoswipe官方文档的 How to build an array of slides from a list of links 部分。

主要的思想就是,根据上面 gallery.js 中生成的链接的list,利用photoswipe生成对应的photoswipe对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
"use strict";

var initPhotoSwipeFromDOM = function(gallerySelector) {

// parse slide data (url, title, size ...) from DOM elements
// (children of gallerySelector)
// Return
// [
// {
// src: "http://imageBed/artwork/src.jpg",
// msrc: "http://imageBed/thumbnail/src.jpg",
// w: 600,
// h: 400,
// title: "这是caption",
// },
// ]
var parseThumbnailElements = function parseThumbnailElements(el) {
el = el.parentNode.parentNode;
var thumbElements = el.getElementsByClassName('thumb'),
numNodes = thumbElements.length,
items = [],
figureEl,
linkEl,
size,
item;
for (var i = 0; i < numNodes; i++) {
figureEl = thumbElements[i]; // <figure> element
// include only element nodes
if (figureEl.nodeType !== 1) {
continue;
}
linkEl = figureEl.children[0]; // <a> element
size = linkEl.getAttribute('data-size').split('x');

// create slide object
item = {
src: linkEl.getAttribute('href'),
w: parseInt(size[0], 10),
h: parseInt(size[1], 10)
};
if (figureEl.children.length > 1) { // <figcaption> content
item.title = figureEl.children[1].innerHTML;
}
if (linkEl.children.length > 0) { // <img> thumbnail element, retrieving thumbnail url
item.msrc = linkEl.children[0].getAttribute('src');
}
item.el = figureEl; // save link to element for getThumbBoundsFn
items.push(item);
}
return items;
};

// find nearest parent element
var closest = function closest(el, fn) {
return el && (fn(el) ? el : closest(el.parentNode, fn));
};

// triggers when user clicks on thumbnail
var onThumbnailsClick = function onThumbnailsClick(e) {
e = e || window.event;
e.preventDefault ? e.preventDefault() : e.returnValue = false;
var eTarget = e.target || e.srcElement;
// find root element of slide
var clickedListItem = closest(eTarget, function(el) {
return el.tagName && el.tagName.toUpperCase() === 'FIGURE';
});
if (!clickedListItem) {
return;
}
// find index of clicked item by looping through all child nodes
// alternatively, you may define index via data- attribute
var clickedGallery = clickedListItem.parentNode,
childNodes = document.getElementsByClassName('thumb'),
numChildNodes = childNodes.length,
nodeIndex = 0,
index;

for (var i = 0; i < numChildNodes; i++) {
if (childNodes[i].nodeType !== 1) {
continue;
}
if (childNodes[i] === clickedListItem) {
index = nodeIndex;
break;
}
nodeIndex++;
}
if (index >= 0) {
// open PhotoSwipe if valid index found
openPhotoSwipe(index, clickedGallery);
}
return false;
};

// parse picture index and gallery index from URL (#&pid=1&gid=2)
function photoswipeParseHash() {
var hash = window.location.hash.substring(1),
params = {};
if (hash.length < 5) {
return params;
}
var vars = hash.split('&');
for (var i = 0; i < vars.length; i++) {
if (!vars[i]) {
continue;
}
var pair = vars[i].split('=');
if (pair.length < 2) {
continue;
}
params[pair[0]] = pair[1];
}
if (params.gid) {
params.gid = parseInt(params.gid, 10);
}
return params;
};

function openPhotoSwipe(index, galleryElement, disableAnimation, fromURL) {
var pswpElement = document.querySelectorAll('.pswp')[0],
gallery,
options,
items;

items = parseThumbnailElements(galleryElement);

// define options (if needed)
options = {
// define gallery index (for URL)
galleryUID: galleryElement.getAttribute('data-pswp-uid'),
getThumbBoundsFn: function getThumbBoundsFn(index) {
// See Options -> getThumbBoundsFn section of documentation for more info
var thumbnail = items[index].el.getElementsByTagName('img')[0],
// find thumbnail
pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
rect = thumbnail.getBoundingClientRect();
return {
x: rect.left,
y: rect.top + pageYScroll,
w: rect.width
};
}
};

// PhotoSwipe opened from URL
if (fromURL) {
if (options.galleryPIDs) {
// parse real index when custom PIDs are used
// http://photoswipe.com/documentation/faq.html#custom-pid-in-url
for (var j = 0; j < items.length; j++) {
if (items[j].pid == index) {
options.index = j;
break;
}
}
} else {
// in URL indexes start from 1
options.index = parseInt(index, 10) - 1;
}
} else {
options.index = parseInt(index, 10);
}
// exit if index not found
if (isNaN(options.index)) {
return;
}
if (disableAnimation) {
options.showAnimationDuration = 0;
}
// Pass data to PhotoSwipe and initialize it
var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);
gallery.init();
};

// loop through all gallery elements and bind events
var galleryElements = document.querySelectorAll(gallerySelector);
for (var i = 0, l = galleryElements.length; i < l; i++) {
galleryElements[i].setAttribute('data-pswp-uid', i + 1);
galleryElements[i].onclick = onThumbnailsClick;
}
// Parse URL and open gallery if it contains #&pid=3&gid=1
var hashData = photoswipeParseHash();
if (hashData.pid && hashData.gid) {
openPhotoSwipe(hashData.pid, galleryElements[hashData.gid - 1], true, true);
}
};

var galleryClass = "photos";
var selector = '.' + galleryClass;
var viewer = function() {
function init() {
initPhotoSwipeFromDOM(selector);
}
return {
init: init
};
}();

module.exports = {
galleryClass,
viewer
};

自动化更新JSON脚本

这里补上了自动化上传图片的脚本。脚本的功能如下

  • 创建相册
  • 插入图片
    • 裁剪图片
    • 压缩图片
    • 更新JSON脚本
    • 上传图片到AWS的S3服务

实现的原理很简单,由AlbumTool.pyS3.py组成。这两个文件放在hexo/album_tool目录下,在同级路径还有一个album的文件夹,针对不同的相册,都有artworksquarethumbnail三个文件夹。用户需要上传图片时,只需要将对应的图片放到artwork路径下,然后运行python AlbumTool.py -a insert就可以了,之后可以按照命令行的提示执行即可。

AlbumTool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#coding: utf-8
import os
import sys
import time
import json
import argparse
import readline
from datetime import datetime
from PIL import Image
from S3 import S3Tool

bucket = "xxx"
imageBed = "https://xxx.s3-ap-southeast-1.amazonaws.com/album"
indexTmpl = """
---
layout: gallery
title: {0}
galleryName: {0}
comments: false
---
"""

class AlbumTool(object):

def __init__(self, bucket, gallery):
self.s3 = S3Tool(bucket)
self.scale = 4
self.gallery = gallery
self.galleryPath = "album/" + gallery
self.artwork = self.galleryPath + "/artwork/"
self.square = self.galleryPath + "/square/"
self.thumbnail = self.galleryPath + "/thumbnail/"
self.data_json = self.galleryPath + "/data.json"

def cut_by_ratio(self, infile, outfile):
"""按照图片长宽进行分割
取中间的部分,裁剪成正方形
"""
im = Image.open(infile)
(w, h) = im.size
region = (0, 0, 0, 0)
if w < h:
w, h = h, w
region = (int(w-h)/2, 0, int(w+h)/2, h)
crop_img = im.crop(region)
crop_img.save(outfile)

def cut(self, image):
"""裁剪图片,将图片裁成正方形"""
print("Cutting the artwork image to square...")
if not os.path.exists(self.square):
os.makedirs(self.square)
self.cut_by_ratio(self.artwork+image, self.square+image)

def compress(self, image):
"""调用压缩图片的函数"""
print("Compressing the square image to thumbnail...")
if not os.path.exists(self.thumbnail):
os.makedirs(self.thumbnail)
img = Image.open(self.square + image)
w, h = img.size
w, h = int(w/self.scale), int(h/self.scale)
img.thumbnail((w, h))
img.save(self.thumbnail + image)

def describe(self, image, caption):
"""
根据当前传入的相册名,图片,描述更新相册的描述JSON文件
"""
# 根据image和caption信息,生成关于image的新dict
print("Updating the data json file...")
img_info = Image.open(self.artwork + image)
width, height = img_info.size
image_dict = {
"name": image,
"caption": caption,
"type": "image",
"date": image[:10],
"width": width,
"height": height
}
item_dict = {
"date": image[:7],
"images": [
image_dict
]
}
# 根据相册名确定原来JSON文件的路径,读取原来的JSON文件并解析
items = []
images = []
index = 0

current = time.strftime('%Y-%m-%d %H:%M-%S', time.localtime(time.time()))
with open(self.data_json) as json_file:
data = json.load(json_file)
items = data["items"]

# 根据当前的文件名,命名为yyyy-mm-dd_name.jpg[png][gif]
# 解析出应该插入的月份,并且更新相应的list,同时加入caption

if items:
for item in items:
if item["date"] != image[:7]:
continue
images = item["images"]
index = items.index(item)
if images:
for img in images:
if img["name"] == image:
print("The file " + image + " already exists")
return
images.insert(0, image_dict)
images.sort(key=lambda image:image["date"], reverse=True)
items[index]["images"] = images
else:
items.insert(0, item_dict)
else:
items.insert(0, item_dict)

items.sort(key=lambda item:item["date"], reverse=True)
data["items"] = items
data["updated"] = current
data["number"] = data["number"] + 1

# 序列化数据结构到JSON文本
with open(self.data_json, 'w', encoding='utf-8') as json_file:
json.dump(data, json_file, ensure_ascii=False, sort_keys=False, indent=4)

def upload(self, image):
print("Uploading the image to AWS S3 service...")
self.s3.upload_file(self.artwork + image)
self.s3.upload_file(self.thumbnail + image)

def sync(self):
os.system("cp " + self.data_json + " ../source/album/" + self.gallery + "/data")

def create(self):
if os.path.exists(self.galleryPath):
print("Error: gallery " + self.gallery + " already exists")
return
else:
os.makedirs(self.galleryPath)
os.makedirs(self.artwork)
os.makedirs(self.square)
os.makedirs(self.thumbnail)

cover = input("Please input the cover image for gallery " + self.gallery + ": ")
description = input("Please input the description for the gallery " + self.gallery + ": ")
current = time.strftime('%Y-%m-%d %H:%M-%S', time.localtime(time.time()))
data_dict = {
"name": self.gallery,
"cover": cover,
"description": description,
"created": current,
"updated": current,
"imageBed": imageBed,
"number": 0,
"items": []
}
with open(self.data_json, 'w', encoding='utf-8') as json_file:
json.dump(data_dict, json_file, ensure_ascii=False, sort_keys=False, indent=4)

os.makedirs("../source/album/" + self.gallery)
with open("../source/album/" + self.gallery + "/index.md", 'w', encoding='utf-8') as f:
f.write(indexTmpl.format(self.gallery, self.gallery))
self.sync()


def insert(self):
image = input("Please input the image file name: ")
caption = input("Please input the caption for the image: ")
self.cut(image)
self.compress(image)
self.describe(image, caption)
self.upload(image)
self.sync()

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Album Tool For Hexo NexT")
parser.add_argument('-a', '--action', type=str, help='action to be executed')
args = parser.parse_args()

gallery = input("Please input the gallery name: ")

album = AlbumTool(bucket, gallery)
if args.action == "create":
album.create()
elif args.action == "insert":
album.insert()

S3.py主要是对boto3进行了封装,配置好AWS的密钥信息之后,可以直接执行这个脚本。如果以后改用阿里云或者其他云服务商的对象存储服务,随之更新即可。

S3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import logging
import boto3
from botocore.exceptions import ClientError

class S3Tool(object):

def __init__(self, bucket):
self.s3_client = boto3.client('s3')
self.bucket = bucket

def upload_file(self, file_name, object_name=None):
"""Upload a file to an S3 bucket

:param file_name: File to upload
:param object_name: S3 object name. If not specified then file_name is used
:return: True if file was uploaded, else False
"""

# If S3 object_name was not specified, use file_name
if object_name is None:
object_name = file_name

# Upload the file
try:
response = self.s3_client.upload_file(file_name, self.bucket, object_name)
except ClientError as e:
logging.error(e)
return False
return True

def download_file(self, file_name, object_name=None):
"""Download a file from an S3 bucket

:param file_name: File to be downloaded as
:param object_name: S3 object name. If not specified then file_name is used
:return: True if file was uploaded, else False
"""

# If S3 object_name was not specified, use file_name
if object_name is None:
object_name = file_name

# Upload the file
try:
response = self.s3_client.download_file(self.bucket, object_name, file_name)
except ClientError as e:
logging.error(e)
return False
return True

实际实现效果

大家可以到 我的相册 页面看看的实际的效果。基本上实现了上述的要求,欢迎评论。

Reference