├── .gitignore ├── CONTRIBUTORS.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README.zh.md ├── api ├── README.md ├── README.zh.md ├── consts.go ├── error.go ├── jsontypes.go ├── path.go └── state.go ├── client ├── check.go ├── client.go ├── common.go ├── state.go ├── upload.go └── url.go ├── common ├── logger │ └── logger.go └── util │ ├── fs.go │ ├── handler.go │ ├── string.go │ └── util.go ├── deploy.md ├── deploy.zh.md ├── entry.go ├── go.mod ├── go.sum ├── init.go ├── plugins ├── README.md ├── README.zh.md ├── args.go ├── fs │ ├── README.md │ ├── README.zh.md │ ├── extra.go │ ├── fs.go │ ├── init.go │ ├── save.go │ ├── state.go │ └── url.go ├── manager.go ├── qiniu │ ├── README.md │ ├── README.zh.md │ ├── init.go │ ├── qiniu.go │ ├── save.go │ ├── state.go │ └── url.go ├── task.go ├── tencent │ ├── ci │ │ ├── README.md │ │ ├── README.zh.md │ │ ├── ci.go │ │ ├── init.go │ │ ├── save.go │ │ ├── state.go │ │ └── url.go │ ├── cos │ │ ├── README.md │ │ ├── README.zh.md │ │ ├── client.go │ │ ├── cos.go │ │ ├── cosargs.go │ │ ├── init.go │ │ ├── save.go │ │ ├── state.go │ │ └── url.go │ └── envvar.go ├── types.go ├── upai │ ├── README.md │ ├── README.zh.md │ ├── init.go │ ├── save.go │ ├── state.go │ ├── upai.go │ └── url.go └── weibo │ ├── README.md │ ├── README.zh.md │ ├── client.go │ ├── extra.go │ ├── init.go │ ├── save.go │ ├── state.go │ ├── url.go │ └── weibo.go ├── rikkac ├── README.md ├── README.zh.md ├── file.go ├── format.go ├── params.go ├── rikkac.go └── worker.go └── server ├── apiserver ├── jsonutil.go ├── start.go ├── state.go ├── upload.go └── url.go ├── start.go └── webserver ├── check.go ├── context.go ├── index.go ├── path.go ├── start.go ├── static.go ├── static ├── css │ ├── common.css │ ├── index.css │ └── view.css ├── image │ ├── favicon.png │ └── rikka.png └── js │ ├── checkForm.js │ ├── copy.js │ ├── getSrc.js │ └── onError.js ├── templates ├── index.html ├── view.html └── viewFinish.html └── view.go /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | **/.DS_Store 3 | 4 | # rikka binary 5 | rikka 6 | rikkac/rikkac 7 | 8 | # rikka binary when debug 9 | debug 10 | 11 | # vscode project config 12 | .vscode/ 13 | 14 | # default uploaded image save dir 15 | files 16 | 17 | # rikkac binary 18 | cli/rikkac 19 | 20 | # git big picture 21 | bp.png 22 | 23 | # goland 24 | .idea/ 25 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Rikka project contributors (sorted alphabetically) 2 | 3 | - [7sDream](https://github.com/7sDream) 4 | - Main maintainer 5 | 6 | - [Cholerae Hu](https://github.com/choleraehyq) 7 | - Delete redundant panic recover 8 | - Fix some bugs of logger package 9 | 10 | - [Codefalling](https://github.com/CodeFalling) 11 | - Rewrite all javascript to ES5 to support more browser 12 | - Add a promise polyfill 13 | 14 | - [ilumer](https://github.com/ilumer) 15 | - Implement Tencent COS v5 plugin 16 | 17 | [Full contributors list](https://github.com/7sDream/rikka/graphs/contributors). 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # --------------- 2 | # Build Stage 3 | # --------------- 4 | 5 | FROM amd64/golang:1 as builder 6 | 7 | ARG VCS_REF 8 | ARG VCS_URL 9 | ARG BUILD_DATE 10 | ARG VERSION 11 | 12 | LABEL org.label-schema.schema-version="1.0" \ 13 | org.label-schema.version=$VERSION \ 14 | org.label-schema.build-date=$BUILD_DATE \ 15 | org.label-schema.vcs-ref=$VCS_REF \ 16 | org.label-schema.vcs-url=$VCS_URL \ 17 | org.label-schema.vcs-type="Git" \ 18 | org.label-schema.license="MIT" \ 19 | org.label-schema.docker.dockerfile="/Dockerfile" \ 20 | org.label-schema.name="Rikka" \ 21 | maintainer="docker@7sdre.am" 22 | 23 | ENV GO111MODULE=on 24 | 25 | WORKDIR /src 26 | COPY . . 27 | 28 | RUN go env -w GOPROXY="https://goproxy.io,direct" && \ 29 | go env -w GOSUMDB="gosum.io+ce6e7565+AY5qEHUk/qmHc5btzW45JVoENfazw8LielDsaI+lEbq6" && \ 30 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v . 31 | 32 | # --------------- 33 | # Final Stage 34 | # --------------- 35 | 36 | FROM amd64/alpine:3 37 | 38 | WORKDIR /root/rikka 39 | 40 | COPY --from=builder /src/rikka rikka 41 | COPY --from=builder /src/server/webserver/templates server/webserver/templates 42 | COPY --from=builder /src/server/webserver/static server/webserver/static 43 | 44 | EXPOSE 80 45 | 46 | ENTRYPOINT ["./rikka"] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 7sDream 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(OS),Windows_NT) # is Windows_NT on XP, 2000, 7, Vista, 10... 2 | detected_OS := Windows 3 | else 4 | detected_OS := $(shell uname) 5 | endif 6 | 7 | ifeq ($(detected_OS),Darwin) # Mac OS X 8 | SED = "gsed" 9 | else 10 | SED = "sed" 11 | endif 12 | 13 | default: rikka-build-test 14 | 15 | # Rikka 16 | 17 | rikka-build-test: rikka-build rikka-test 18 | 19 | rikka-build: 20 | go build . 21 | 22 | rikka-test: 23 | -./rikka -port 8000 -fsDebugSleep 5000 -https -level 0 24 | 25 | qiniu: rikka-build 26 | -./rikka -port 8000 -plugin qiniu -bname rikka-qiniu -bhost odbw8jckg.bkt.clouddn.com 27 | 28 | upai: rikka-build 29 | -./rikka -port 8000 -plugin upai -bname rikka-upai -bhost rikka-upai.b0.upaiyun.com 30 | 31 | # Docker 32 | 33 | IMAGE_NAME = 7sdream/rikka 34 | OLD_VERSION = $(shell docker images | $(SED) -n 's:$(IMAGE_NAME) \+\([0-9.]\+\).*:\1:gp') 35 | HAS_LATEST = $(shell docker images | $(SED) -n 's:$(IMAGE_NAME) \+\(latest\).*:\1:gp') 36 | NEW_VERSION = $(shell $(SED) -n 's:\t*Version = "\([0-9.]\+\).*":\1:p' api/consts.go) 37 | GIT_COMMIT = $(strip $(shell git rev-parse --short HEAD)) 38 | 39 | version: 40 | @echo "Current git commit: $(GIT_COMMIT)" 41 | @echo "Will delete $(OLD_VERSION) $(HAS_LATEST)" 42 | @echo "Will build $(NEW_VERSION) latest" 43 | 44 | confirm: 45 | @bash -c "read -s -n 1 -p 'Press any key to continue, Ctrl+C to stop'" 46 | 47 | delete: 48 | ifneq ($(OLD_VERSION),) 49 | docker rmi $(IMAGE_NAME):$(OLD_VERSION) 50 | endif 51 | ifneq ($(HAS_LATEST),) 52 | docker rmi $(IMAGE_NAME):latest 53 | endif 54 | -docker rmi $(IMAGE_NAME):$(NEW_VERSION) 55 | 56 | build: version confirm delete clean 57 | docker build \ 58 | --build-arg VERSION=$(NEW_VERSION) \ 59 | --build-arg VCS_URL=$(shell git config --get remote.origin.url) \ 60 | --build-arg VCS_REF=$(GIT_COMMIT) \ 61 | --build-arg BUILD_DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") \ 62 | -t $(IMAGE_NAME):$(NEW_VERSION) . 63 | docker tag $(IMAGE_NAME):$(NEW_VERSION) $(IMAGE_NAME):latest 64 | 65 | just-push: 66 | docker push $(IMAGE_NAME) 67 | 68 | push: build just-push 69 | 70 | # Clean 71 | 72 | clean: 73 | rm -f ./rikka 74 | rm -rf files/ 75 | rm -f debug 76 | rm -f rikkac/rikkac 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rikka - A simple image share system 2 | 3 | ![][badge-version-img] ![][badge-info-img] ![][badge-license-img] 4 | 5 | [中文版][readme-zh] 6 | 7 | Rikka is written in Golang primarily, and provide Docker image. 8 | 9 | Rikka image has been published to [DockerHub][image-in-docker-hub], just try it! 10 | 11 | Badges above shows latest version and size of Rikka image. 12 | 13 | ## Introduction 14 | 15 | Rikka(`りっか` in Japanese, sound like `/ɾʲikka/`, not `/rikka/`)is a integral personal image share system, includes: 16 | 17 | - A web application (See [Demo](#demo) section) 18 | - A REST API server (See [API Doc][api-doc]) 19 | - A CLI tool named Rikkac based on the API (See [Rikkac Doc][rikkac-doc]) 20 | - Image save plugins (See [Plugins Doc][plugins-doc] to get all available plugins) 21 | 22 | Other parts not written in Golang (in plan): 23 | 24 | - Android client 25 | - iOS client 26 | 27 | ## Feature and Advantage 28 | 29 | 1. Simple and minimalist, no upload history 30 | 2. Image address can be copied to various formats 31 | 3. Many available image save plugins, such as weibo, QinNiu, UPai, Tencent Cloud, etc. 32 | 4. REST API provided 33 | 4. Modular Web server and API server 34 | 5. CLI tool provided 35 | 6. **Only guarantee support for recent versions of Chrome/Firefox/Safari** 36 | 7. Cute homepage image 37 | 8. An active maintainer :) 38 | 39 | ## Demo 40 | 41 | There is a [Demo site][demo] built with Rikka, ~~password is `rikka`, just try it.~~ Because the free docker service provider I used stop it's free plan, the demo is on my personal VPS now. So the password is not given anymore, but you can also visit it and have a look :) 42 | 43 | homepage: 44 | 45 | ![homepage][home] 46 | 47 | Click `Choose` button to choose an image. 48 | 49 | Input password`rikka`. 50 | 51 | Click `Upload` button. 52 | 53 | If no error happened, you will be redirect to preview page: 54 | 55 | ![view_page][view] 56 | 57 | You will see a "Please wait" message If you uploaded a large file and save process is not finished, just wait a second. 58 | 59 | When you see image url, you can click `Src`, `Markdown`, `HTML`, `RST`, `BBCode` button to copy image url in that format. 60 | 61 | **But**: Once you close this page, you can't get it back except from browser history(Or you save the url). 62 | 63 | This is intentional. The main design concept is simple, just `Upload-Copy-Close-Paste`, then you can forget Rikka. 64 | 65 | BTW: The preview image of Demo site is saved in Rikka too. (But Github will put images which in Markdown files into its own CDN to accelerate access) 66 | 67 | ## Plugins 68 | 69 | Truly image save back-end of Rikka is written as plugins, can be specified by `-plugin` option. 70 | 71 | Please see [Rikka Plugins Doc][plugins-doc] for available plugins. 72 | 73 | ## API 74 | 75 | See [Rikka API Doc][api-doc]. 76 | 77 | ## CLI - Rikkac 78 | 79 | Rikkac is a CLI tool for Rikka based on Rikka's REST API. 80 | 81 | Build, install, configure and use guide can be found in [Rikkac Doc][rikkac-doc]. 82 | 83 | ## Deploy 84 | 85 | Want deploy Rikka system of you own? Check [Rikka Deploy Doc][deploy-doc] for deploy guide. 86 | 87 | ## Contribution 88 | 89 | - Fork me 90 | - Create a new branch from dev branch 91 | - Add your code, comment, document and meaningful commit message 92 | - Add yourself to CONTRIBUTION.md and describe your work 93 | - PR to dev branch 94 | 95 | Thanks all contributors! 96 | 97 | You can see a list of contributors in [CONTRIBUTIONS.md][contributors]. 98 | 99 | ## Acknowledgements 100 | 101 | - Thanks Golang and her developers 102 | - Thanks Visual Studio Code and her developers 103 | - Thanks open source 104 | 105 | ## License 106 | 107 | All code of Rikka system are open source, based on MIT license. 108 | 109 | See [LICENSE][license]. 110 | 111 | [readme-zh]: https://github.com/7sDream/rikka/blob/master/README.zh.md 112 | 113 | [badge-info-img]: https://img.shields.io/docker/image-size/7sdream/rikka 114 | [badge-version-img]: https://img.shields.io/docker/v/7sdream/rikka 115 | [badge-license-img]: https://img.shields.io/github/license/7sdream/rikka 116 | 117 | [image-in-docker-hub]: https://hub.docker.com/r/7sdream/rikka/ 118 | 119 | [demo]: https://rikka.7sdre.am/ 120 | [home]: https://rikka.7sdre.am/files/56c3ae9d-4d96-49c8-bc03-5104214a1ac8.png 121 | [view]: https://rikka.7sdre.am/files/97bebf3b-9fb8-4b0c-a156-4b92b1951ae4.png 122 | 123 | [api-doc]: https://github.com/7sDream/rikka/tree/master/api 124 | [rikkac-doc]: https://github.com/7sDream/rikka/tree/master/rikkac 125 | [plugins-doc]: https://github.com/7sDream/rikka/tree/master/plugins 126 | [deploy-doc]: https://github.com/7sDream/rikka/blob/master/deploy.md 127 | 128 | [contributors]: https://github.com/7sDream/rikka/blob/master/CONTRIBUTORS.md 129 | [license]: https://github.com/7sDream/rikka/blob/master/LICENSE 130 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # Rikka - 极简图床系统 2 | 3 | ![][badge-version-img] ![][badge-info-img] ![][badge-license-img] 4 | 5 | [English version][readme-en] 6 | 7 | Rikka 主要使用 Go 语言编写,并提供 Docker 镜像。 8 | 9 | Rikka 的镜像已经发布到了 [DockerHub][image-in-docker-hub], 直接开始用吧。 10 | 11 | 最新版本号和镜像大小见上面的徽章。 12 | 13 | ## 简介 14 | 15 | Rikka(因为是日文罗马音,读音类似`莉卡`而不是`瑞卡`)是一套完整的个人图床系统,她包括: 16 | 17 | - 一个 Web 应用(详见 [Demo](#demo) 一节) 18 | - 一个 REST API 后端(详见 [API 文档][api-doc]) 19 | - 基于 API 的命令行工具 Rikkac(详见 [Rikkac 文档][rikkac-doc]) 20 | - 图片的实际储存插件(查看[插件文档][plugins-doc] 来获取所有可用插件的列表) 21 | 22 | 计划实现的其他非 Go 语言的系统组件: 23 | 24 | - Android 客户端 25 | - iOS 客户端 26 | 27 | ## 特点 28 | 29 | 1. 极简,不保存上传历史 30 | 2. 支持将图片链接复制成多种格式 31 | 3. 文件储存部分插件化,有很多可用的插件,比如:新浪微博,七牛云,又拍云,腾讯云等 32 | 4. 提供 API 33 | 4. Web 服务和 REST API 服务模块化 34 | 5. CLI 工具 35 | 6. **只保证支持较新版本的 Chrome/Firefox/Safari** 36 | 7. 首页标志很可爱 37 | 8. 维护者貌似很活跃 :) 38 | 39 | ## Demo 40 | 41 | 这里有一个使用 Rikka 建立的[网站 Demo][demo],~~密码是 `rikka`~~,由于 DaoCloud 现在不能免费用了,所以现在这里的 demo 其实是我自己用的,所以大家只能看看主页了。 42 | 43 | 主页大概长这样: 44 | 45 | ![homepage][home] 46 | 47 | 点击 `Choose` 按钮选一张图片。 48 | 49 | 输入密码 `rikka`。 50 | 51 | 点击上传按钮。 52 | 53 | 上传完成后你将转到查看页面: 54 | 55 | ![view_page][view] 56 | 57 | 如果文件过大,还没有保存完毕的话会看到等待提示,等一下就好。 58 | 59 | 等地址出现后,点击 `Src`, `Markdown`, `HTML`, `RST`,`BBCode` 按钮可以复制对应格式的文本,然后你可以把它粘贴到其他地方。 60 | 61 | 但是注意:如果你关闭了这个页面,除了浏览器的历史记录(或者你保存了这个网址),网站并没有提供其他让你找到以前上传的图片的方法。 62 | 63 | 这是有意为之的,因为 Rikka 的主要设计的理念就是简单, `上传-复制-关闭-粘贴`,之后就再也不用管了。 64 | 65 | PS:你看到的这些预览图也是由 Rikka 储存的哟。(不过放到 Github 之后会被 Github 弄到 CDN 上去) 66 | 67 | ## 插件 68 | 69 | Rikka 的真实储存后端使用插件形式编写。可通过 `-plugin` 参数设置。 70 | 71 | 请看 [Rikka 插件文档][plugins-doc] 查看目前可用的插件。 72 | 73 | ## API 74 | 75 | 请看 [Rikka API 文档][api-doc]。 76 | 77 | ## CLI - Rikkac 78 | 79 | Rikkac 是基于 Rikka 的 REST API 写的 Rikka CLI 工具。 80 | 81 | 编译、安装、配置和使用方法请看 [Rikkac 文档][rikkac-doc]。 82 | 83 | ## 部署 84 | 85 | 想部署自己的 Rikka 系统?请看 [Rikka 部署文档][deploy-doc]。 86 | 87 | ## 协助开发 88 | 89 | - Fork 90 | - 从 dev 分支新建一个分支 91 | - 写代码,注释和文档,并使用有意义的 commit message 提交 92 | - 将自己加入 CONTRIBUTIONS.md,并且描述你做了什么 93 | - PR 到 dev 分支 94 | 95 | 感谢所有协助开发的朋友! 96 | 97 | 在 [CONTRIBUTIONS.md][contributors] 里可以看到贡献者名单。 98 | 99 | ## 致谢 100 | 101 | - 感谢 Go 编程语言以及她的开发团队 102 | - 感谢 Visual Studio Code 编辑器和她的开发团队 103 | - 感谢开源精神 104 | 105 | ## License 106 | 107 | Rikka 系统的所有代码均基于 MIT 协议开源。 108 | 109 | 详见 [LICENSE][license] 文件。 110 | 111 | [readme-en]: https://github.com/7sDream/rikka/blob/master/README.md 112 | 113 | [badge-info-img]: https://img.shields.io/docker/image-size/7sdream/rikka 114 | [badge-version-img]: https://img.shields.io/docker/v/7sdream/rikka 115 | [badge-license-img]: https://img.shields.io/github/license/7sdream/rikka 116 | 117 | [image-in-docker-hub]: https://hub.docker.com/r/7sdream/rikka/ 118 | 119 | [demo]: https://rikka.7sdre.am/ 120 | [home]: https://rikka.7sdre.am/files/56c3ae9d-4d96-49c8-bc03-5104214a1ac8.png 121 | [view]: https://rikka.7sdre.am/files/97bebf3b-9fb8-4b0c-a156-4b92b1951ae4.png 122 | 123 | [api-doc]: https://github.com/7sDream/rikka/blob/master/api/README.zh.md 124 | [rikkac-doc]: https://github.com/7sDream/rikka/blob/master/rikkac/README.zh.md 125 | [plugins-doc]: https://github.com/7sDream/rikka/blob/master/plugins/README.zh.md 126 | [deploy-doc]: https://github.com/7sDream/rikka/blob/master/deploy.zh.md 127 | 128 | [contributors]: https://github.com/7sDream/rikka/blob/master/CONTRIBUTORS.md 129 | [license]: https://github.com/7sDream/rikka/blob/master/LICENSE 130 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # Rikka API Introduction 2 | 3 | [中文版][version-zh] 4 | 5 | Note: API was added into Rikka after version 0.1.0 6 | 7 | BTW,Rikka disable CORS by default, if you need use api in other domain, please add `-corsAllowOrigin '*'` argument on start (available after version 0.8.0). 8 | 9 | ## Upload 10 | 11 | ### Path 12 | 13 | `POST /api/upload` 14 | 15 | ### Params 16 | 17 | `Content-type` muse be `multipart/form-data`. 18 | 19 | - `uploadFile"`:the image file 20 | - `password"`:Rikka password 21 | - `from"`:request source, value must be `api` 22 | 23 | ### Return Value 24 | 25 | A JSON contains task ID if you request successfully, then you can get task state and final image url use the ID. 26 | 27 | ```json 28 | { 29 | "TaskID": "2016-09-02-908463234" 30 | } 31 | ``` 32 | 33 | A error JSON when request failed, error message format can be found in the end of article. 34 | 35 | ## Get Task State 36 | 37 | ### Path 38 | 39 | `GET /api/state/` 40 | 41 | ### Params 42 | 43 | None 44 | 45 | ### Return Value 46 | 47 | A state JSON like bellow if query successfully. 48 | 49 | ```json 50 | { 51 | "TaskID": "2016-09-02-908463234", 52 | "State": "state name", 53 | "StateCode": 1, 54 | "Description": "state Description" 55 | } 56 | ``` 57 | 58 | A error JSON when query failed, error message format can be found in the end of article. 59 | 60 | ## Get Image URL 61 | 62 | ### Path 63 | 64 | `GET /api/url/` 65 | 66 | ### Params 67 | 68 | None 69 | 70 | ### Return Value 71 | 72 | A JSON contains image URL like bellow if query successfully. 73 | 74 | ```json 75 | { 76 | "URL": "http://127.0.0.1/files/2016-09-02-908463234" 77 | } 78 | ``` 79 | 80 | A error JSON when query failed, error message format can be found in the end of article. 81 | 82 | ## Error JSON format 83 | 84 | If API request error, return error JSON like bellow: 85 | 86 | ```json 87 | { 88 | "Error": "error message" 89 | } 90 | ``` 91 | 92 | [version-zh]: https://github.com/7sDream/rikka/blob/master/api/README.zh.md 93 | -------------------------------------------------------------------------------- /api/README.zh.md: -------------------------------------------------------------------------------- 1 | # Rikka API 简介 2 | 3 | [English version][version-en] 4 | 5 | 注意:Rikka 0.1.0 版本之后才有 API 功能。 6 | 7 | 另外,Rikka 默认不开启 CORS 支持。如果需要在其他前端页面使用上传 API,请在启动时添加 `-corsAllowOrigin '*'` 参数(0.8.0 版本后可用)。 8 | 9 | ## 上传 10 | 11 | ### 地址 12 | 13 | `POST /api/upload` 14 | 15 | ### 参数 16 | 17 | `Content-type` 为 `multipart/form-data`。 18 | 19 | - `uploadFile"`:要上传的文件 20 | - `password"`:Rikka 的密码 21 | - `from"`:必须项,指示请求来源 22 | 23 | ### 返回值 24 | 25 | 上传成功则返回任务 ID,可通过任务 ID 获取任务状态和图片地址 26 | 27 | ```json 28 | { 29 | "TaskID": "2016-09-02-908463234" 30 | } 31 | ``` 32 | 33 | 失败返回错误,格式见后。 34 | 35 | ## 获取任务状态 36 | 37 | ### 地址 38 | 39 | `GET /api/state/` 40 | 41 | ### 参数 42 | 43 | 无 44 | 45 | ### 返回值 46 | 47 | 查询成功则返回任务状态。 48 | 49 | ```json 50 | { 51 | "TaskID": "2016-09-02-908463234", 52 | "State": "state name", 53 | "StateCode": 1, 54 | "Description": "state Description" 55 | } 56 | ``` 57 | 58 | 失败返回错误,格式见后。 59 | 60 | ## 获取图片地址 61 | 62 | ### 地址 63 | 64 | `GET /api/url/` 65 | 66 | ### 参数 67 | 68 | 无 69 | 70 | ### 返回值 71 | 72 | 查询成功则返回图片原始 URL。 73 | 74 | ```json 75 | { 76 | "URL": "http://127.0.0.1/files/2016-09-02-908463234" 77 | } 78 | ``` 79 | 80 | 失败返回错误,格式见后。 81 | 82 | ## 错误格式 83 | 84 | 如果 API 请求出错,返回格式如下: 85 | 86 | ```json 87 | { 88 | "Error": "error message" 89 | } 90 | ``` 91 | 92 | [version-en]: https://github.com/7sDream/rikka/blob/master/api/README.md 93 | -------------------------------------------------------------------------------- /api/consts.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | const ( 4 | // Version of Rikka 5 | Version = "0.8.0" 6 | 7 | // FormKeyFile is file field name when upload image 8 | FormKeyFile = "uploadFile" 9 | // FormKeyPWD is password field name when upload image 10 | FormKeyPWD = "password" 11 | // FormKeyFrom is from field name when upload image 12 | FormKeyFrom = "from" 13 | 14 | // FromWebsite is a value of FromKeyFrom, means request comes from website 15 | FromWebsite = "website" 16 | // FromAPI is a value of FromKeyFrom, means request comes from REST API 17 | FromAPI = "api" 18 | ) 19 | -------------------------------------------------------------------------------- /api/error.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // API error messages 4 | var ( 5 | // Upload errors 6 | ErrPwdErrMsg = "error password" 7 | InvalidFromArgErrMsg = "from argument can only be website or api" 8 | NotAImgFileErrMsg = "the file you upload is not an image" 9 | 10 | // Task errors 11 | TaskNotExistErrMsg = "task not exist" 12 | TaskAlreadyExistErrMsg = "task already exist" 13 | TaskNotFinishErrMsg = "task is not finished" 14 | ) 15 | -------------------------------------------------------------------------------- /api/jsontypes.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // State shows a state of task. 4 | type State struct { 5 | TaskID string 6 | StateCode int 7 | State string 8 | Description string 9 | } 10 | 11 | // Error struct used to build json from error string. 12 | type Error struct { 13 | Error string 14 | } 15 | 16 | // URL struct used to build json from error URL. 17 | type URL struct { 18 | URL string 19 | } 20 | 21 | // TaskId struct used to build json from taskID. 22 | type TaskId struct { 23 | TaskId string 24 | } 25 | -------------------------------------------------------------------------------- /api/path.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // API Path 4 | const ( 5 | UploadPath = "/api/upload" 6 | StatePath = "/api/state/" 7 | URLPath = "/api/url/" 8 | ) 9 | -------------------------------------------------------------------------------- /api/state.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // Built-in state code and state str, description. 4 | // Must be used in finish and error state. 5 | const ( 6 | StateError = "error" 7 | StateErrorCode = -1 8 | 9 | StateFinish = "finish" 10 | StateFinishCode = 0 11 | StateFinishDescription = "file upload task finish" 12 | 13 | StateCreate = "just created" 14 | StateCreateCode = 1 15 | StateCreateDescription = "the task is created just now, waiting for next operate" 16 | ) 17 | 18 | // BuildCreateState build a standard just-create state from taskID. 19 | func BuildCreateState(taskID string) *State { 20 | return &State{ 21 | TaskID: taskID, 22 | State: StateCreate, 23 | StateCode: StateCreateCode, 24 | Description: StateCreateDescription, 25 | } 26 | } 27 | 28 | // BuildFinishState build a standard finished state from taskID. 29 | func BuildFinishState(taskID string) *State { 30 | return &State{ 31 | TaskID: taskID, 32 | State: StateFinish, 33 | StateCode: StateFinishCode, 34 | Description: StateFinishDescription, 35 | } 36 | } 37 | 38 | // BuildErrorState build a standard error state from taskID and description. 39 | func BuildErrorState(taskID string, description string) *State { 40 | return &State{ 41 | TaskID: taskID, 42 | State: StateError, 43 | StateCode: StateErrorCode, 44 | Description: description, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/check.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/7sDream/rikka/server/apiserver" 9 | ) 10 | 11 | func CheckFile(absFilePath string) ([]byte, error) { 12 | fileContent, err := ioutil.ReadFile(absFilePath) 13 | if err != nil { 14 | l.Debug("Error happened when try to read file", absFilePath, ":", err) 15 | return nil, err 16 | } 17 | l.Debug("Read file", absFilePath, "content successfully") 18 | 19 | fileType := http.DetectContentType(fileContent) 20 | if _, ok := apiserver.IsAccepted(fileType); !ok { 21 | errMsg := "File" + absFilePath + "is not a acceptable image file, it is" + fileType 22 | l.Debug(errMsg) 23 | return nil, errors.New(errMsg) 24 | } 25 | l.Debug("Fie", absFilePath, "type check passed:", fileType) 26 | 27 | return fileContent, nil 28 | } 29 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/7sDream/rikka/common/logger" 5 | ) 6 | 7 | var ( 8 | l = logger.NewLogger("[Client]") 9 | ) 10 | -------------------------------------------------------------------------------- /client/common.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/7sDream/rikka/api" 10 | ) 11 | 12 | func mustBeErrorJSON(content []byte) error { 13 | pError := &api.Error{} 14 | var err error 15 | if err = json.Unmarshal(content, pError); err != nil { 16 | l.Debug("Error happened when decode response to error json:", err) 17 | return err 18 | } 19 | if pError.Error == "" { 20 | l.Debug("Unable to decode response to error json:", "result is empty") 21 | return errors.New("Unable to decode Rikka server response to error json" + string(content)) 22 | } 23 | return errors.New(pError.Error) 24 | } 25 | 26 | func checkRes(url string, res *http.Response) ([]byte, error) { 27 | resContent, err := ioutil.ReadAll(res.Body) 28 | if err != nil { 29 | l.Debug("Error happened when read response body:", err) 30 | return nil, err 31 | } 32 | l.Debug("Get response content of", url, "successfully:", string(resContent)) 33 | 34 | if res.StatusCode != http.StatusOK { 35 | l.Debug("Rikka return a non-ok status code", res.StatusCode) 36 | return nil, mustBeErrorJSON(resContent) 37 | } 38 | l.Debug("Rikka response OK when request", url) 39 | 40 | return resContent, nil 41 | } 42 | -------------------------------------------------------------------------------- /client/state.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/7sDream/rikka/api" 10 | ) 11 | 12 | func GetState(host string, taskID string) (*api.State, error) { 13 | url := host + api.StatePath + taskID 14 | l.Debug("Build state request url:", url) 15 | 16 | res, err := http.Get(url) 17 | if err != nil { 18 | l.Debug("Error happened when send state request to url", url, ":", err) 19 | return nil, err 20 | } 21 | l.Debug("Send state request successfully") 22 | 23 | resContent, err := checkRes(url, res) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | pState := &api.State{} 29 | if err = json.Unmarshal(resContent, pState); err != nil || pState.TaskID == "" { 30 | l.Debug("Decode response to state json failed, try to decode to error json") 31 | return nil, mustBeErrorJSON(resContent) 32 | } 33 | l.Debug("Decode response to state json successfully") 34 | return pState, nil 35 | } 36 | 37 | func WaitFinish(host string, taskID string) error { 38 | for { 39 | state, err := GetState(host, taskID) 40 | if err != nil { 41 | l.Debug("Error happened when get state of task", taskID) 42 | continue 43 | } 44 | 45 | l.Debug("State of task", taskID, "is:", state.Description) 46 | 47 | if state.StateCode == api.StateErrorCode { 48 | l.Debug("Rikka return a error task state:", state.Description) 49 | return errors.New(state.Description) 50 | } 51 | 52 | if state.StateCode == api.StateFinishCode { 53 | return nil 54 | } 55 | 56 | l.Debug("State is not finished, will retry after 1 second...") 57 | 58 | time.Sleep(1 * time.Second) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/upload.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "mime/multipart" 7 | "net/http" 8 | "path/filepath" 9 | 10 | "github.com/7sDream/rikka/api" 11 | ) 12 | 13 | var uploadClient *http.Client 14 | 15 | func init() { 16 | uploadClient = &http.Client{} 17 | } 18 | 19 | func createUploadRequest(url string, path string, content []byte, params map[string]string) (*http.Request, error) { 20 | body := &bytes.Buffer{} 21 | writer := multipart.NewWriter(body) 22 | 23 | part, err := writer.CreateFormFile(api.FormKeyFile, filepath.Base(path)) 24 | if err != nil { 25 | l.Debug("Error happened when create form file:", err) 26 | return nil, err 27 | } 28 | l.Debug("Create form writer successfully") 29 | 30 | if _, err = part.Write(content); err != nil { 31 | l.Debug("Error happened when write file content to form:", err) 32 | return nil, err 33 | } 34 | l.Debug("Write file content to form file successfully") 35 | 36 | for key, val := range params { 37 | if err = writer.WriteField(key, val); err != nil { 38 | l.Debug("Error happened when try to write params [", key, "=", val, "] to form:", err) 39 | return nil, err 40 | } 41 | l.Debug("Write params [", key, "=", val, "] to form successfully") 42 | } 43 | 44 | if err = writer.Close(); err != nil { 45 | l.Debug("Error happened when close form writer:", err) 46 | return nil, err 47 | } 48 | l.Debug("Close form writer successfully") 49 | 50 | req, err := http.NewRequest("POST", url, body) 51 | if err != nil { 52 | l.Debug("Error happened when create post request:", err) 53 | return nil, err 54 | } 55 | l.Debug("Create request successfully") 56 | 57 | req.Header.Set("Content-Type", writer.FormDataContentType()) 58 | 59 | return req, nil 60 | } 61 | 62 | func getParams(password string) map[string]string { 63 | params := map[string]string{ 64 | api.FormKeyFrom: api.FromAPI, 65 | api.FormKeyPWD: password, 66 | } 67 | 68 | l.Debug("Build params:", params) 69 | 70 | return params 71 | } 72 | 73 | func Upload(host string, path string, content []byte, password string) (string, error) { 74 | 75 | url := host + api.UploadPath 76 | 77 | l.Debug("Build upload url:", url) 78 | 79 | req, err := createUploadRequest(url, path, content, getParams(password)) 80 | if err != nil { 81 | return "", err 82 | } 83 | 84 | res, err := uploadClient.Do(req) 85 | if err != nil { 86 | l.Debug("Error happened when try to send upload request:", err) 87 | return "", err 88 | } 89 | l.Debug("Send upload request successfully") 90 | 91 | resContent, err := checkRes(url, res) 92 | if err != nil { 93 | return "", err 94 | } 95 | 96 | pTaskID := &api.TaskId{} 97 | if err = json.Unmarshal(resContent, pTaskID); err != nil || pTaskID.TaskId == "" { 98 | l.Debug("Decode response to taskID json failed, try to decode to error message") 99 | return "", mustBeErrorJSON(resContent) 100 | } 101 | l.Debug("Decode response to taskID json successfully") 102 | 103 | return pTaskID.TaskId, nil 104 | } 105 | -------------------------------------------------------------------------------- /client/url.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/7sDream/rikka/api" 8 | ) 9 | 10 | func GetURL(host string, taskID string) (*api.URL, error) { 11 | url := host + api.URLPath + taskID 12 | l.Debug("Build url request url:", url) 13 | 14 | res, err := http.Get(url) 15 | if err != nil { 16 | l.Debug("Error happened when send url get request:", err) 17 | return nil, err 18 | } 19 | l.Debug("Send upload request successfully") 20 | 21 | resContent, err := checkRes(url, res) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | pURL := &api.URL{} 27 | 28 | if err := json.Unmarshal(resContent, pURL); err != nil || pURL.URL == "" { 29 | l.Debug("Decode response to url json failed, try to decode to error message") 30 | return nil, mustBeErrorJSON(resContent) 31 | } 32 | l.Debug("Decode response to url json successfully") 33 | return pURL, nil 34 | } 35 | -------------------------------------------------------------------------------- /common/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | // Logger is a util class to print log in different level like DEBUG. 10 | // It has 4 level: DEBUG, INFO, WARN, ERROR, 11 | // and 2 exception logger: PANIC and FATAL 12 | type Logger struct { 13 | prefix string 14 | trueLogger *log.Logger 15 | } 16 | 17 | // Logger levels 18 | const ( 19 | LevelDebug int = iota 20 | LevelInfo 21 | LevelWarn 22 | LevelError 23 | ) 24 | 25 | var ( 26 | l = NewLogger("[Logger]") 27 | 28 | currentLevel = LevelDebug 29 | ) 30 | 31 | // NewLogger create a new top level logger based on prefix. 32 | func NewLogger(prefix string) *Logger { 33 | flag := log.Ldate | log.Ltime | log.Lmicroseconds 34 | return &Logger{ 35 | prefix: prefix, 36 | trueLogger: log.New(os.Stdout, "", flag), 37 | } 38 | } 39 | 40 | // Debug print log message as DEBUG level. 41 | // if you set log level higher than LevelDebug, no message will be print. 42 | func (logger *Logger) Debug(data ...interface{}) { 43 | if currentLevel <= LevelDebug { 44 | logger.trueLogger.Print("[DEBUG] ", logger.prefix, " ", fmt.Sprintln(data...)) 45 | } 46 | } 47 | 48 | // Info print log message as INFO level. 49 | // If you set log level higher than LevelInfo, no message will be print. 50 | func (logger *Logger) Info(data ...interface{}) { 51 | if currentLevel <= LevelInfo { 52 | logger.trueLogger.Print("[ Info] ", logger.prefix, " ", fmt.Sprintln(data...)) 53 | } 54 | } 55 | 56 | // Warn print log message as WARN level. 57 | // If you set log level higher than LevelWarn, no message will be print. 58 | func (logger *Logger) Warn(data ...interface{}) { 59 | if currentLevel <= LevelWarn { 60 | logger.trueLogger.Print("[ WARN] ", logger.prefix, " ", fmt.Sprintln(data...)) 61 | } 62 | } 63 | 64 | // Error print log message as ERROR level. 65 | // This function do not create panic or fatal, it just print error message. 66 | // If you want get a runtime panic or fatal, use Logger.Panic or Logger.Fatal instead. 67 | func (logger *Logger) Error(data ...interface{}) { 68 | if currentLevel <= LevelError { 69 | logger.trueLogger.Print("[ERROR] ", logger.prefix, " ", fmt.Sprintln(data...)) 70 | } 71 | } 72 | 73 | // Panic print log message, and create a panic use the message. 74 | func (logger *Logger) Panic(data ...interface{}) { 75 | logger.trueLogger.Panic("[PANIC]", logger.prefix, " ", fmt.Sprint(data...)) 76 | } 77 | 78 | // Fatal print log message, and create a fatal use the message. 79 | func (logger *Logger) Fatal(data ...interface{}) { 80 | logger.trueLogger.Fatal("FATAL", logger.prefix, " ", fmt.Sprint(data...)) 81 | } 82 | 83 | // SubLogger create a new logger based on the logger. 84 | // Prefix string of new logger will be concat of old and provided argument. 85 | func (logger *Logger) SubLogger(prefix string) (subLogger *Logger) { 86 | return NewLogger(fmt.Sprintf("%s %s", logger.prefix, prefix)) 87 | } 88 | 89 | // SetLevel set the minimum level that message will be print out 90 | func SetLevel(level int) { 91 | if LevelDebug <= level && level < LevelError { 92 | currentLevel = level 93 | } else { 94 | l.Error("Set logger level", level, "failed, accepted range is", LevelInfo, "to", LevelError) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /common/util/fs.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // CheckExist check if a file or dir is Exist. 8 | func CheckExist(filepath string) bool { 9 | if _, err := os.Stat(filepath); os.IsNotExist(err) { 10 | return false 11 | } 12 | return true 13 | } 14 | 15 | // IsDir check if the path is a file, false when not exist or is a dir. 16 | func IsDir(path string) bool { 17 | if CheckExist(path) { 18 | stat, _ := os.Stat(path) 19 | return stat.IsDir() 20 | } 21 | return false 22 | } 23 | 24 | // IsFile check if a path a file, false when not exist or is a file. 25 | func IsFile(path string) bool { 26 | if CheckExist(path) { 27 | stat, _ := os.Stat(path) 28 | return !stat.IsDir() 29 | } 30 | return false 31 | } 32 | -------------------------------------------------------------------------------- /common/util/handler.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "html/template" 8 | "io/ioutil" 9 | "net" 10 | "net/http" 11 | pathUtil "path/filepath" 12 | "strings" 13 | 14 | "github.com/7sDream/rikka/common/logger" 15 | ) 16 | 17 | // GetTaskIDByRequest gets last part of url path as a taskID and return it. 18 | func GetTaskIDByRequest(r *http.Request) string { 19 | splitPath := strings.Split(r.URL.Path, "/") 20 | filename := splitPath[len(splitPath)-1] 21 | return filename 22 | } 23 | 24 | // ErrHandle is a simple error handle function. 25 | // See ErrHandleWithCode, this func use `http.StatusInternalServerError` as code. 26 | func ErrHandle(w http.ResponseWriter, err error) bool { 27 | return ErrHandleWithCode(w, err, http.StatusInternalServerError) 28 | } 29 | 30 | // ErrHandleWithCode is a simple error handle function. 31 | // If err is an error, write code to header and write error message to response and return true. 32 | // Else (err is nil), don't do anything and return false. 33 | func ErrHandleWithCode(w http.ResponseWriter, err error, code int) bool { 34 | if err != nil { 35 | http.Error(w, err.Error(), code) 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | // GetClientIP get client ip address from a http request. 42 | // Try to get ip from x-forwarded-for header first, 43 | // If key not exist in header, try to get ip:host from r.RemoteAddr 44 | // split it to ip:port and return ip, If error happened, return 0.0.0.0 45 | func GetClientIP(r *http.Request) string { 46 | defer func() { 47 | if r := recover(); r != nil { 48 | l.Error("Unexpected panic happened when get client ip:", r) 49 | } 50 | }() 51 | 52 | forwardIP := r.Header.Get("X-FORWARDED-FOR") 53 | if forwardIP != "" { 54 | return forwardIP 55 | } 56 | 57 | socket := r.RemoteAddr 58 | host, _, err := net.SplitHostPort(socket) 59 | if err != nil { 60 | l.Warn("Error happened when get IP address :", err) 61 | return "0.0.0.0" 62 | } 63 | return host 64 | } 65 | 66 | // CheckMethod check if request method is as excepted. 67 | // If not, write the status "MethodNotAllow" to header, "Method Not Allowed." to response and return false. 68 | // Else don't do anything and return true. 69 | func CheckMethod(w http.ResponseWriter, r *http.Request, excepted string) bool { 70 | if r.Method != excepted { 71 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) 72 | return false 73 | } 74 | return true 75 | } 76 | 77 | // RenderTemplate is a shortcut function to render template to response. 78 | func RenderTemplate(templatePath string, w http.ResponseWriter, data interface{}) error { 79 | t, err := template.ParseFiles(templatePath) 80 | if ErrHandle(w, err) { 81 | l.Error("Error happened when parse template file", templatePath, ":", err) 82 | return err 83 | } 84 | 85 | // a buff that ues in execute, if error happened, 86 | // error message will not be write to truly response 87 | buff := bytes.NewBuffer([]byte{}) 88 | err = t.Execute(buff, data) 89 | 90 | // error happened, write a generic error message to response 91 | if err != nil { 92 | l.Error("Error happened when execute template", t, "with data", fmt.Sprintf("%+v", data), ":", err) 93 | ErrHandle(w, errors.New("error when render template")) 94 | return err 95 | } 96 | 97 | _, err = w.Write(buff.Bytes()) 98 | 99 | return err 100 | } 101 | 102 | func RenderTemplateString(templateString string, w http.ResponseWriter, data interface{}) error { 103 | t := template.New("_") 104 | t, err := t.Parse(templateString) 105 | if ErrHandle(w, err) { 106 | l.Error("Error happened when parse template string", templateString, ":", err) 107 | return err 108 | } 109 | buff := bytes.NewBuffer([]byte{}) 110 | err = t.Execute(buff, data) 111 | if err != nil { 112 | l.Error("Error happened when execute template", t, "with data", fmt.Sprintf("%+v", data), ":", err) 113 | ErrHandle(w, errors.New("error when render template")) 114 | return err 115 | } 116 | _, err = w.Write(buff.Bytes()) 117 | return err 118 | } 119 | 120 | // RenderJson is a shortcut function to write JSON data to response, and set the header Content-Type. 121 | func RenderJson(w http.ResponseWriter, data []byte, code int) (err error) { 122 | w.Header().Set("Content-Type", "application/json") 123 | w.WriteHeader(code) 124 | _, err = w.Write(data) 125 | return err 126 | } 127 | 128 | // MustBeOr404 check if URL path is as excepted. 129 | // If not equal, write 404 to header, "404 not fount" to response, and return false. 130 | // Else don't do anything and return true. 131 | func MustBeOr404(w http.ResponseWriter, r *http.Request, path string) bool { 132 | if r.URL.Path != path { 133 | http.NotFound(w, r) 134 | return false 135 | } 136 | return true 137 | } 138 | 139 | // MustExistOr404 check if a file is exist. 140 | // If not, write 404 to header, "404 not fount" to response, and return false. 141 | // Else don't do anything and return true. 142 | //noinspection GoUnusedExportedFunction 143 | func MustExistOr404(w http.ResponseWriter, r *http.Request, filepath string) bool { 144 | if !CheckExist(filepath) { 145 | http.NotFound(w, r) 146 | return false 147 | } 148 | return true 149 | } 150 | 151 | // DisableListDir accept a handle func and return a handle that not allow list dir. 152 | func DisableListDir(log *logger.Logger, h http.HandlerFunc) http.HandlerFunc { 153 | if log == nil { 154 | l.Warn("Get a nil logger in function DisableListDirFunc") 155 | log = l 156 | } 157 | return func(w http.ResponseWriter, r *http.Request) { 158 | if strings.HasSuffix(r.URL.Path, "/") { 159 | log.Warn(GetClientIP(r), "try to list dir", r.URL.Path) 160 | http.NotFound(w, r) 161 | } else { 162 | h(w, r) 163 | } 164 | } 165 | } 166 | 167 | // ContextCreator accept a request and return a context, used in TemplateRenderHandler. 168 | type ContextCreator func(r *http.Request) interface{} 169 | 170 | // TemplateStringRenderHandler is a shortcut function that generate a http.HandlerFunc. 171 | // The generated func use contextCreator to create context and render the templateString as template. 172 | // If contextCreator is nil, nil will be used as context. 173 | func TemplateStringRenderHandler(templateName string, templateString string, contextCreator ContextCreator, log *logger.Logger) http.HandlerFunc { 174 | if log == nil { 175 | l.Warn("Get a nil logger in function TemplateRenderHandler") 176 | log = l 177 | } 178 | return func(w http.ResponseWriter, r *http.Request) { 179 | ip := GetClientIP(r) 180 | log.Info("Receive a template render request of", templateName, "from ip", ip) 181 | 182 | var data interface{} 183 | if contextCreator != nil { 184 | data = contextCreator(r) 185 | } else { 186 | data = nil 187 | } 188 | 189 | err := RenderTemplateString(templateString, w, data) 190 | 191 | if err != nil { 192 | log.Warn("Error happened when render template string", templateString, "with data", fmt.Sprintf("%#v", data), "to", ip, ": ", err) 193 | } 194 | 195 | log.Info("Render template", templateName, "to", ip, "successfully") 196 | } 197 | } 198 | 199 | // TemplateRenderHandler is a shortcut function that generate a http.HandlerFunc. 200 | // The generated func use contextCreator to create context and render the templatePath template file. 201 | // If contextCreator is nil, nil will be used as context. 202 | func TemplateRenderHandler(templatePath string, contextCreator ContextCreator, log *logger.Logger) http.HandlerFunc { 203 | templateBytes, err := ioutil.ReadFile(templatePath) 204 | if err != nil { 205 | l.Fatal("Error when read template file", templatePath, ":", err) 206 | } 207 | templateName := pathUtil.Base(templatePath) 208 | templateString := string(templateBytes) 209 | return TemplateStringRenderHandler(templateName, templateString, contextCreator, log) 210 | } 211 | 212 | // RequestFilter accept a http.HandlerFunc and return a new one 213 | // which only accept path is pathMustBe and method is methodMustBe. 214 | // Error message in new handler will be print with logger log, if log is nil, will use default logger. 215 | // If pathMustBe or methodMustBe is empty string, no check will be performed. 216 | func RequestFilter(pathMustBe string, methodMustBe string, log *logger.Logger, handlerFunc http.HandlerFunc) http.HandlerFunc { 217 | return func(w http.ResponseWriter, r *http.Request) { 218 | 219 | if log == nil { 220 | l.Warn("Get a nil logger in function RequestFilter") 221 | log = l 222 | } 223 | 224 | ip := GetClientIP(r) 225 | 226 | if pathMustBe != "" { 227 | if !MustBeOr404(w, r, pathMustBe) { 228 | log.Warn(ip, "visit a non-exist page", r.URL.Path, ", excepted is /") 229 | return 230 | } 231 | } 232 | 233 | if methodMustBe != "" { 234 | if !CheckMethod(w, r, methodMustBe) { 235 | log.Warn(ip, "visit page", r.URL.Path, "with method", r.Method, ", only", methodMustBe, "is allowed") 236 | return 237 | } 238 | } 239 | 240 | handlerFunc(w, r) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /common/util/string.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strings" 5 | "unicode/utf8" 6 | ) 7 | 8 | // MaskString keep first `showNum` count char of a string `str` and change all remaining chars to "*" 9 | func MaskString(str string, showNum int) string { 10 | var res string 11 | var i int 12 | var c rune 13 | for i, c = range str { 14 | if i < showNum { 15 | res += string(c) 16 | } else { 17 | break 18 | } 19 | } 20 | if i != showNum { 21 | i++ 22 | } 23 | length := utf8.RuneCountInString(str) 24 | if i < length { 25 | res += strings.Repeat("*", length-i) 26 | } 27 | return res 28 | } 29 | -------------------------------------------------------------------------------- /common/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/7sDream/rikka/common/logger" 7 | ) 8 | 9 | var ( 10 | l = logger.NewLogger("[Util]") 11 | ) 12 | 13 | // GetEnvWithCheck will get a env var, print it, and return it. 14 | // If the var is empty, will raise a Fatal. 15 | func GetEnvWithCheck(name, key string, log *logger.Logger) string { 16 | if log == nil { 17 | log = l 18 | } 19 | value := os.Getenv(key) 20 | if value == "" { 21 | log.Fatal("No", name, "provided, please add it to your env var use the name", key) 22 | } 23 | log.Info("Args", name, "=", MaskString(value, 5)) 24 | return value 25 | } 26 | -------------------------------------------------------------------------------- /deploy.md: -------------------------------------------------------------------------------- 1 | # Deploy 2 | 3 | [中文版][version-zh] 4 | 5 | The following ways use default plugin `fs` as example. 6 | 7 | ## Way 1: Build in you VPS 8 | 9 | 1. `go get -u -d github.com/7sDream/rikka` 10 | 2. `cd $GOPATH/src/github.com/7sDream/rikka` 11 | 3. `go build .` 12 | 4. `./rikka -port 80 -pwd yourPassword` 13 | 14 | You can use `./rikka --help` to get more options and make you own launch command. 15 | 16 | Because port 80 wll be used, may you need `sudo` prefix. 17 | 18 | Then you can open your browser to test Rikka. 19 | 20 | ## Way 2: Use Docker 21 | 22 | 1. `docker pull 7sdream/rikka` 23 | 2. `docker run -d -p 80:80 7sdream/rikka -pwd yourPassword` 24 | 25 | You can set option based on you requirements. 26 | 27 | Rikka image expose 80 port, you can map it based on needs. 28 | 29 | Then you can open your browser to test Rikka. 30 | 31 | Note: If you stop/remove Rikka container, the images you uploaded will be deleted too. If you want keep those files, please read next section: Use Volume. 32 | 33 | ### Use Volume 34 | 35 | Docker provide a feature called Volume. We can use it to keep out images. 36 | 37 | Usage: 38 | 39 | 1. Create volume:`docker volume create --name rikka_files` 40 | 2. Add this option when you start Rikka:`-v rikka_files:/go/src/github.com/7sDream/rikka/files` 41 | 42 | BTW: You can use `-dir` option of plugin `fs` to set image save dir, like bellow: 43 | 44 | `docker run -d -P -v rikka_files:/data --name rikka 7sdream/rikka -pwd 12345 -dir /data` 45 | 46 | So you need't input a long mount path like `/go/src/github.com/7sDream/rikka/files`. 47 | 48 | ## Way 3: Use Docker Cloud Service Provider 49 | 50 | For example, you can use free-plan of DaoCloud to deploy a Rikka server. 51 | 52 | See [DaoCloud Deploy Guide][daocloud-guide] for detail. 53 | 54 | ## Use Other plugin 55 | 56 | Main steps are the same. 57 | 58 | See [Plugins Doc] for options for different plugins. 59 | 60 | [version-zh]: https://github.com/7sDream/rikka/blob/master/deploy.zh.md 61 | 62 | [daocloud-guide]: https://github.com/7sDream/rikka/wiki/%E5%9C%A8-DaoCloud-%E4%B8%8A%E5%85%8D%E8%B4%B9%E9%83%A8%E7%BD%B2-Rikka 63 | [plugins-doc]: https://github.com/7sDream/rikka/tree/master/plugins 64 | -------------------------------------------------------------------------------- /deploy.zh.md: -------------------------------------------------------------------------------- 1 | # 部署 2 | 3 | [English version][version-en] 4 | 5 | 以下部署方法均以默认 `fs` 插件为例。 6 | 7 | ## 方式 1: 在你的 VPS 上编译 8 | 9 | 1. `go get -u -d github.com/7sDream/rikka` 10 | 2. `cd $GOPATH/src/github.com/7sDream/rikka` 11 | 3. `go build .` 12 | 4. `./rikka -port 80 -pwd yourPassword` 13 | 14 | 最后一步具体的命令可查看 `./rikka -h` 之后根据自己需要设置。 15 | 16 | 因为要使用 80 端口,所以可能需要在启动命令前加上 `sudo`。 17 | 18 | 之后你就可以用浏览器打开看看效果了。 19 | 20 | ## 方式 2: 使用 Docker 21 | 22 | 1. `docker pull 7sdream/rikka` 23 | 2. `docker run -d -p 80:80 7sdream/rikka -pwd yourPassword` 24 | 25 | 同样可以根据需要设定参数。至于 image expose 的是 80 端口,请根据需要进行映射。 26 | 27 | 打开浏览器访问你的 IP 或域名试用看看吧。 28 | 29 | PS: 如果你停止/删除了 Rikka 容器,你上传的照片也会一起被删除。如果你不想这样,请参考下一节:使用数据卷。 30 | 31 | ### 使用数据卷 32 | 33 | Docker 提供了数据卷的功能,这样就不用怕我们上传的图片会应用关闭之后丢失了。 34 | 35 | 使用方法: 36 | 37 | 1. 创建数据卷:`docker volume create --name rikka_files` 38 | 2. 在启动 Rikka 容器时加上如下参数:`-v rikka_files:/go/src/github.com/7sDream/rikka/files` 39 | 40 | PS:你可以使用 Rikka `fs` 插件的 `-dir` 参数指定文件储存位置,比如这样: 41 | 42 | `docker run -d -P -v rikka_files:/data --name rikka 7sdream/rikka -pwd 12345 -dir /data` 43 | 44 | 这样就不用把挂载路径设的太长了。 45 | 46 | ## 方式 3: 使用 Docker 云服务提供商 47 | 48 | 比如,我们可以用 DaoCloud 的免费配额来部署一个 Rikka 服务。 49 | 50 | 详细步骤请看 [DaoCloud 部署教程](https://github.com/7sDream/rikka/wiki/%E5%9C%A8-DaoCloud-%E4%B8%8A%E5%85%8D%E8%B4%B9%E9%83%A8%E7%BD%B2-Rikka)。 51 | 52 | ## 使用其他插件 53 | 54 | 主要步骤和上述相同。 55 | 56 | 不同插件的不同启动参数请参考[插件文档][plugins-doc]。 57 | 58 | [version-en]: https://github.com/7sDream/rikka/blob/master/deploy.md 59 | 60 | [daocloud-guide]: https://github.com/7sDream/rikka/wiki/%E5%9C%A8-DaoCloud-%E4%B8%8A%E5%85%8D%E8%B4%B9%E9%83%A8%E7%BD%B2-Rikka 61 | [plugins-doc]: https://github.com/7sDream/rikka/blob/master/plugins/README.zh.md 62 | -------------------------------------------------------------------------------- /entry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/7sDream/rikka/common/logger" 5 | "github.com/7sDream/rikka/common/util" 6 | "github.com/7sDream/rikka/plugins" 7 | "github.com/7sDream/rikka/server" 8 | ) 9 | 10 | // Logger of this package 11 | var ( 12 | l = logger.NewLogger("[Entry]") 13 | ) 14 | 15 | // Main entry point 16 | func main() { 17 | // print launch args 18 | l.Info("Start rikka with arg:") 19 | l.Info("\t bind to socket", socket) 20 | l.Info("\t password", util.MaskString(*argPassword, 3)) 21 | l.Info("\t max file size", *argMaxSizeByMB, "MB") 22 | l.Info("\t plugin", *argPluginStr) 23 | l.Info("\t log level", *argLogLevel) 24 | 25 | l.Info("Load plugin...") 26 | plugins.Load(thePlugin) 27 | 28 | // start Rikka servers (this call is Sync) 29 | server.StartRikka(socket, *argPassword, *argMaxSizeByMB, *argHTTPS, *argCertDir, *argAllowOrigin) 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/7sDream/rikka 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/jeremywangjun/image-go-sdk v2.0.4+incompatible 7 | github.com/qiniu/go-sdk/v7 v7.9.5 8 | github.com/satori/go.uuid v1.2.0 9 | github.com/tencentyun/cos-go-sdk-v5 v0.7.25 10 | github.com/upyun/go-sdk/v3 v3.0.2 11 | ) 12 | 13 | require ( 14 | github.com/bitly/go-simplejson v0.5.0 // indirect 15 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect 16 | github.com/google/go-querystring v1.1.0 // indirect 17 | github.com/kr/pretty v0.1.0 // indirect 18 | github.com/mozillazg/go-httpheader v0.3.0 // indirect 19 | github.com/tencentyun/go-sdk v2.0.4+incompatible // indirect 20 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= 2 | github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= 3 | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= 4 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 5 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 8 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 9 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 10 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 11 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 12 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 13 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 | github.com/jeremywangjun/image-go-sdk v2.0.4+incompatible h1:QgGpbe0a3OSCzuaLyel4nxxZBjkKWuyOWAowftyDe/s= 15 | github.com/jeremywangjun/image-go-sdk v2.0.4+incompatible/go.mod h1:8ROP0hKpwkYUw7kRYr7wTaolL/N6T5PRwppm8m8pIKE= 16 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 17 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 18 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 19 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 20 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 21 | github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= 22 | github.com/mozillazg/go-httpheader v0.3.0 h1:3brX5z8HTH+0RrNA1362Rc3HsaxyWEKtGY45YrhuINM= 23 | github.com/mozillazg/go-httpheader v0.3.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/qiniu/go-sdk/v7 v7.9.5 h1:hxsdSmRAfN8hp77OUjjDgKHyxjHpFlW5fC2rHC6hMRQ= 26 | github.com/qiniu/go-sdk/v7 v7.9.5/go.mod h1:Eeqk1/Km3f1MuLUUkg2JCSg/dVkydKbBvEdJJqFgn9g= 27 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 28 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 29 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 31 | github.com/tencentyun/cos-go-sdk-v5 v0.7.25 h1:CvVHCy46TKU1ibT7FLb2i46CIrmwWPZIcky/m+7WzQQ= 32 | github.com/tencentyun/cos-go-sdk-v5 v0.7.25/go.mod h1:wQBO5HdAkLjj2q6XQiIfDSP8DXDNrppDRw2Kp/1BODA= 33 | github.com/tencentyun/go-sdk v2.0.4+incompatible h1:0uLI3F/DHuHqdWL2RKN8wtRWF5NJwA01AjXQEHHxOLU= 34 | github.com/tencentyun/go-sdk v2.0.4+incompatible/go.mod h1:9sWrHEQHdyq5ztiwaJif1SE5KjoweQSyND3wPbv4sFU= 35 | github.com/upyun/go-sdk/v3 v3.0.2 h1:Ke+iOipK5CT0xzMwsgJsi7faJV7ID4lAs+wrH1RH0dA= 36 | github.com/upyun/go-sdk/v3 v3.0.2/go.mod h1:P/SnuuwhrIgAVRd/ZpzDWqCsBAf/oHg7UggbAxyZa0E= 37 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 39 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 40 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 41 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 42 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 43 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | 10 | "github.com/7sDream/rikka/common/logger" 11 | "github.com/7sDream/rikka/plugins" 12 | "github.com/7sDream/rikka/plugins/fs" 13 | "github.com/7sDream/rikka/plugins/qiniu" 14 | "github.com/7sDream/rikka/plugins/tencent/ci" 15 | "github.com/7sDream/rikka/plugins/tencent/cos" 16 | "github.com/7sDream/rikka/plugins/upai" 17 | "github.com/7sDream/rikka/plugins/weibo" 18 | ) 19 | 20 | var ( 21 | // Map from plugin name to object 22 | pluginMap = make(map[string]plugins.RikkaPlugin) 23 | 24 | // Command line arguments var 25 | argBindIPAddress *string 26 | argPort *int 27 | argPassword *string 28 | argMaxSizeByMB *float64 29 | argPluginStr *string 30 | argLogLevel *int 31 | argHTTPS *bool 32 | argCertDir *string 33 | argAllowOrigin *string 34 | 35 | // concat socket from ip address and port 36 | socket string 37 | 38 | // The used plugin 39 | thePlugin plugins.RikkaPlugin 40 | ) 41 | 42 | // --- Init and check --- 43 | 44 | func createSignalHandler(handlerFunc func()) (func(), chan os.Signal) { 45 | signalChain := make(chan os.Signal, 1) 46 | 47 | return func() { 48 | for range signalChain { 49 | handlerFunc() 50 | } 51 | }, signalChain 52 | } 53 | 54 | // registerSignalHandler register a handler for process Ctrl + C 55 | func registerSignalHandler(handlerFunc func()) { 56 | signalHandler, channel := createSignalHandler(handlerFunc) 57 | signal.Notify(channel, os.Interrupt) 58 | go signalHandler() 59 | } 60 | 61 | func init() { 62 | 63 | registerSignalHandler(func() { 64 | l.Info("Receive interrupt signal") 65 | l.Info("Rikka have to go to sleep, see you tomorrow") 66 | os.Exit(0) 67 | }) 68 | 69 | initPluginList() 70 | 71 | initArgVars() 72 | 73 | flag.Parse() 74 | 75 | l.Info("Args bindIP =", *argBindIPAddress) 76 | l.Info("Args port =", *argPort) 77 | l.Info("Args password =", *argPassword) 78 | l.Info("Args maxFileSize =", *argMaxSizeByMB, "MB") 79 | l.Info("Args loggerLevel =", *argLogLevel) 80 | l.Info("Args https =", *argHTTPS) 81 | l.Info("Args cert dir =", *argCertDir) 82 | l.Info("Args plugin =", *argPluginStr) 83 | 84 | if *argPort == 0 { 85 | if *argHTTPS { 86 | *argPort = 443 87 | } else { 88 | *argPort = 80 89 | } 90 | } 91 | 92 | if *argBindIPAddress == ":" { 93 | socket = *argBindIPAddress + strconv.Itoa(*argPort) 94 | } else { 95 | socket = *argBindIPAddress + ":" + strconv.Itoa(*argPort) 96 | } 97 | 98 | logger.SetLevel(*argLogLevel) 99 | 100 | runtimeEnvCheck() 101 | } 102 | 103 | func initPluginList() { 104 | pluginMap["fs"] = fs.Plugin 105 | pluginMap["qiniu"] = qiniu.Plugin 106 | pluginMap["upai"] = upai.Plugin 107 | pluginMap["weibo"] = weibo.Plugin 108 | pluginMap["tccos"] = cos.Plugin 109 | pluginMap["tcci"] = ci.Plugin 110 | } 111 | 112 | func initArgVars() { 113 | argBindIPAddress = flag.String("bind", ":", "Bind ip address, use : for all address") 114 | argPort = flag.Int("port", 0, "Server port, 0 means use 80 when disable HTTPS, 443 when enable") 115 | argPassword = flag.String("pwd", "rikka", "The password need provided when upload") 116 | argMaxSizeByMB = flag.Float64("size", 5, "Max file size by MB") 117 | argLogLevel = flag.Int( 118 | "level", logger.LevelInfo, 119 | fmt.Sprintf("Log level, from %d to %d", logger.LevelDebug, logger.LevelError), 120 | ) 121 | argHTTPS = flag.Bool("https", false, "Use HTTPS") 122 | argCertDir = flag.String("certDir", ".", "Where to find HTTPS cert files(cert.pem, key.pem)") 123 | argAllowOrigin = flag.String("corsAllowOrigin", "", "Enable upload api CORS support, default is empty(disable). Set this to a origin, or * to enable for all origin") 124 | // Get name array of all available plugins, show in `rikka -h` 125 | pluginNames := make([]string, 0, len(pluginMap)) 126 | for k := range pluginMap { 127 | pluginNames = append(pluginNames, k) 128 | } 129 | argPluginStr = flag.String( 130 | "plugin", "fs", 131 | "What plugin use to save file, selected from "+fmt.Sprintf("%v", pluginNames), 132 | ) 133 | } 134 | 135 | func runtimeEnvCheck() { 136 | l.Info("Check runtime environment") 137 | 138 | l.Debug("Try to find plugin", *argPluginStr) 139 | 140 | // Make sure plugin be selected exist 141 | if plugin, ok := pluginMap[*argPluginStr]; ok { 142 | thePlugin = plugin 143 | l.Debug("Plugin", *argPluginStr, "found") 144 | } else { 145 | l.Fatal("Plugin", *argPluginStr, "not exist") 146 | } 147 | 148 | l.Info("All runtime environment check passed") 149 | } 150 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Rikka Plugin System 2 | 3 | [中文版][version-zh] 4 | 5 | Rikka back-end image save is powered by plugins. 6 | 7 | ## fs Plugin 8 | 9 | This is default plugin of Rikka,it save image file to server where Rikka live directly, and run a static file server for those files. 10 | 11 | Refer to [fs Plugin Doc][fs-doc] for plugin options. 12 | 13 | ## Qiniu Cloud Plugin 14 | 15 | This plugin use Qiniu Cloud CDN to store your image. 16 | 17 | Refer to [Qiniu Plugin Doc][qiniu-doc] for plugin options. 18 | 19 | ## UPai Cloud Plugin 20 | 21 | This plugin use UPai Cloud CDN to store your image. 22 | 23 | Refer to [UPai Plugin Doc][upai-doc]. 24 | 25 | ## Sina Weibo Plugin 26 | 27 | This plugin use Sina weibo to store your image. 28 | 29 | Refer to [Weibo plugin Doc][weibo-doc]. 30 | 31 | ## Tencent COS Plugin 32 | 33 | This plugin use Cloud Object Service (COS) of Tencent to store image files. 34 | 35 | Refer to [TC-COS Plugin Doc][tccos-doc]. 36 | 37 | ## Tencent CI Plugin 38 | 39 | This plugin use Cloud Image (CI) of Tencent to store image files. 40 | 41 | Refer to [TC-CI Plugin Doc][tcci-doc]. 42 | 43 | [version-zh]: https://github.com/7sDream/rikka/blob/master/plugins/README.zh.md 44 | 45 | [fs-doc]: https://github.com/7sDream/rikka/tree/master/plugins/fs 46 | [qiniu-doc]: https://github.com/7sDream/rikka/tree/master/plugins/qiniu 47 | [upai-doc]: https://github.com/7sDream/rikka/tree/master/plugins/upai 48 | [weibo-doc]: https://github.com/7sDream/rikka/tree/master/plugins/weibo 49 | [tccos-doc]: https://github.com/7sDream/rikka/tree/master/plugins/tencent/cos 50 | [tcci-doc]: https://github.com/7sDream/rikka/tree/master/plugins/tencent/ci 51 | -------------------------------------------------------------------------------- /plugins/README.zh.md: -------------------------------------------------------------------------------- 1 | # Rikka 插件系统 2 | 3 | [English version][version-en] 4 | 5 | Rikka 后端实际图片的储存使用插件形式处理。 6 | 7 | ## fs 插件 8 | 9 | 这是 Rikka 的默认插件,它直接将上传的图片储存在部署 Rikka 的服务器上,并且使用一个静态文件 Server 来提供这些图片。 10 | 11 | 请看 [fs 插件文档][fs-doc] 查看插件的配置参数。 12 | 13 | ## Qiniu 七牛云插件 14 | 15 | 这个插件使用七牛云 CDN 来储存你上传的图片。 16 | 17 | 请看 [Qiniu 插件文档][qiniu-doc] 查看插件的配置参数。 18 | 19 | ## Upai 又拍云插件 20 | 21 | 这个插件使用又拍云 CDN 来储存你上传的图片。 22 | 23 | 请看 [UPai 插件文档][upai-doc] 查看插件配置参数。 24 | 25 | ## Weibo 新浪微博插件 26 | 27 | 这个插件使用新浪微博发送微博时的上传图片接口作为图片的最终储存方式。 28 | 29 | 请看 [Weibo 插件文档][weibo-doc] 查看插件配置参数。 30 | 31 | [version-en]: https://github.com/7sDream/rikka/blob/master/plugins/README.md 32 | 33 | ## TC-COS 腾讯 COS 插件 34 | 35 | 这个插件使用腾讯云的对象储存服务(Cloud Object Service, COS)来储存图片。 36 | 37 | 请看 [TC-COS 插件文档][tccos-doc] 查看插件配置参数。 38 | 39 | ## TC-CI 腾讯 CI 插件 40 | 41 | 这个插件使用腾讯云的万象优图(Cloud Image, CI)服务来储存图片。 42 | 43 | 请看 [TC-CI 插件文档][tcci-doc] 查看插件配置参数。 44 | 45 | [fs-doc]: https://github.com/7sDream/rikka/tree/master/plugins/fs/README.zh.md 46 | [qiniu-doc]: https://github.com/7sDream/rikka/tree/master/plugins/qiniu/README.zh.md 47 | [upai-doc]: https://github.com/7sDream/rikka/tree/master/plugins/upai/README.zh.md 48 | [weibo-doc]: https://github.com/7sDream/rikka/tree/master/plugins/weibo/README.zh.md 49 | [tccos-doc]: https://github.com/7sDream/rikka/tree/master/plugins/tencent/cos/README.zh.md 50 | [tcci-doc]: https://github.com/7sDream/rikka/tree/master/plugins/tencent/ci/README.zh.md 51 | -------------------------------------------------------------------------------- /plugins/args.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "flag" 5 | "net/url" 6 | "strings" 7 | ) 8 | 9 | // Common flags for cloud plugins like qiniu and upai 10 | var ( 11 | ArgBucketName = flag.String("bname", "", "Bucket name to store image") 12 | ArgBucketHost = flag.String("bhost", "", "Bucket host") 13 | ArgBucketPath = flag.String("bpath", "", "Where the image will be save in bucket") 14 | ) 15 | 16 | // CheckCommonArgs will check if bname and bhost is set and log their value 17 | func CheckCommonArgs(needName, needHost bool) { 18 | l.Info("Args bucket name =", *ArgBucketName) 19 | if needName && *ArgBucketName == "" { 20 | l.Fatal("No bucket name provided, please add option -bname") 21 | } 22 | 23 | l.Info("Args bucket host =", *ArgBucketHost) 24 | if needHost && *ArgBucketHost == "" { 25 | l.Fatal("No bucket host provided, please add option -bhost") 26 | } 27 | 28 | l.Info("Args bucket path =", *ArgBucketPath) 29 | } 30 | 31 | // GetBucketName get the name of bucket where image will be stored 32 | func GetBucketName() string { 33 | return *ArgBucketName 34 | } 35 | 36 | // GetBucketHost get the host of bucket where image will be stored 37 | func GetBucketHost() string { 38 | bucketAddr := *ArgBucketHost 39 | if !strings.HasPrefix(bucketAddr, "http") { 40 | bucketAddr = "http://" + bucketAddr 41 | } 42 | 43 | pURL, err := url.Parse(bucketAddr) 44 | if err != nil { 45 | l.Fatal("Invalid bucket host", bucketAddr, ":", err) 46 | } 47 | 48 | bucketAddr = pURL.Scheme + "://" + pURL.Host 49 | 50 | return bucketAddr 51 | } 52 | 53 | // GetBucketPath get the save path of bucket which image will be stored to 54 | func GetBucketPath() string { 55 | bucketPrefix := *ArgBucketPath 56 | if strings.HasPrefix(bucketPrefix, "/") { 57 | bucketPrefix = bucketPrefix[1:] 58 | } 59 | if len(bucketPrefix) > 0 && !strings.HasSuffix(bucketPrefix, "/") { 60 | bucketPrefix = bucketPrefix + "/" 61 | } 62 | 63 | return bucketPrefix 64 | } 65 | -------------------------------------------------------------------------------- /plugins/fs/README.md: -------------------------------------------------------------------------------- 1 | # fs Plugin 2 | 3 | [中文版][version-zh] 4 | 5 | Inner name `fs`, default plugin of Rikka. 6 | 7 | ## Description 8 | 9 | it save image file to server where Rikka live directly, and run a static file server for those files. 10 | 11 | ## Options 12 | 13 | `-dir` set file dir where image saved. Default is `files` folder under work dir. If you are using Docker or deploying Rikka at Docker Cloud Server Provider, you can set it to a position easy to volume mount, like `/data`. 14 | 15 | `-fsDebugSleep` Not for common use, it make a sleep before copy file to dir, simulate a long time operation,for javascript AJAX tests. In microsecond. 16 | 17 | If your website support https,you can add `-https` argument to make fs plugin return image url with https protocol. 18 | 19 | [version-zh]: https://github.com/7sDream/rikka/blob/master/plugins/fs/README.zh.md 20 | -------------------------------------------------------------------------------- /plugins/fs/README.zh.md: -------------------------------------------------------------------------------- 1 | # fs 插件 2 | 3 | [English version][version-en] 4 | 5 | 插件内部名 `fs`,是 Rikka 的默认插件。 6 | 7 | ## 说明 8 | 9 | 它直接将上传的图片储存在部署 Rikka 的服务器上,并且使用一个静态文件 Server 来提供这些图片。 10 | 11 | ## 参数 12 | 13 | `-dir` 参数指定文件存放位置。默认位置是当前目录下的 `files` 文件夹。如果你使用 Docker 或在 Docker 云服务上部署的话,可以设置成 `/data` 之类便于挂载的位置。 14 | 15 | `-fsDebugSleep` 一般用不到,是让 fs 插件在复制文件前暂停一段时间,模拟耗时操作,便于测试 javascript AJAX 的。单位是 ms。 16 | 17 | 如果你的域名支持 https,请设置参数 `-https` 来使 fs 模块返回的 https 协议的 url。 18 | 19 | [version-en]: https://github.com/7sDream/rikka/blob/master/plugins/fs/README.md 20 | -------------------------------------------------------------------------------- /plugins/fs/extra.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/7sDream/rikka/common/util" 7 | "github.com/7sDream/rikka/plugins" 8 | ) 9 | 10 | const ( 11 | fileURLPath = "/files/" 12 | ) 13 | 14 | // ExtraHandlers return value will be add to http handle list. 15 | // In fs plugin, we start a static file server to serve image file we accepted in /files/taskID path. 16 | func (fsp fsPlugin) ExtraHandlers() (handlers []plugins.HandlerWithPattern) { 17 | // only accept GET method 18 | requestFilterFileServer := util.RequestFilter( 19 | "", "GET", l, 20 | // disable list dir 21 | util.DisableListDir( 22 | l, 23 | // Strip prefix path 24 | http.StripPrefix( 25 | // reserve last / 26 | fileURLPath[:len(fileURLPath)-1], 27 | // get a base file server 28 | http.FileServer(http.Dir(imageDir)), 29 | ).ServeHTTP, 30 | ), 31 | ) 32 | 33 | handlers = []plugins.HandlerWithPattern{ 34 | { 35 | Pattern: fileURLPath, Handler: requestFilterFileServer, 36 | }, 37 | } 38 | 39 | return handlers 40 | } 41 | -------------------------------------------------------------------------------- /plugins/fs/fs.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/7sDream/rikka/plugins" 7 | ) 8 | 9 | // plugin type 10 | type fsPlugin struct{} 11 | 12 | var ( 13 | l = plugins.SubLogger("[FS]") 14 | 15 | argFilesDir = flag.String("dir", "files", "Where files will be save when use fs plugin.") 16 | argFsDebugSleep = flag.Int("fsDebugSleep", 0, "Debug: sleep some ms before copy file to fs, used to test javascript ajax") 17 | 18 | imageDir string 19 | 20 | // Plugin is the main plugin instance. 21 | Plugin = fsPlugin{} 22 | ) 23 | -------------------------------------------------------------------------------- /plugins/fs/init.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "os" 5 | pathUtil "path/filepath" 6 | 7 | "github.com/7sDream/rikka/common/util" 8 | ) 9 | 10 | // Init is the plugin init function, will be called when plugin be load. 11 | func (fsp fsPlugin) Init() { 12 | // where to store file 13 | l.Info("Start plugin fs") 14 | 15 | l.Info("Args dir =", *argFilesDir) 16 | l.Info("Args fsDebugSleep =", *argFsDebugSleep) 17 | 18 | absFilesDir, err := pathUtil.Abs(*argFilesDir) 19 | if err == nil { 20 | l.Debug("Abs path of image file dir:", absFilesDir) 21 | imageDir = absFilesDir 22 | } else { 23 | l.Fatal("A error happened when change image dir to absolute path:", err) 24 | } 25 | // if target dir not exist, create it 26 | if util.CheckExist(absFilesDir) { 27 | l.Debug("Image file dir already exist") 28 | } else { 29 | l.Debug("Image file dir not exist, try to create it") 30 | err = os.MkdirAll(absFilesDir, 0755) 31 | if err == nil { 32 | l.Debug("Create dir", absFilesDir, "successfully") 33 | } else { 34 | l.Fatal("A error happened when try to create image dir:", err) 35 | } 36 | } 37 | 38 | l.Info("Fs plugin start successfully") 39 | } 40 | -------------------------------------------------------------------------------- /plugins/fs/save.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io" 5 | "mime/multipart" 6 | "os" 7 | pathUtil "path/filepath" 8 | "time" 9 | 10 | "github.com/7sDream/rikka/api" 11 | "github.com/7sDream/rikka/common/util" 12 | "github.com/7sDream/rikka/plugins" 13 | 14 | "github.com/satori/go.uuid" 15 | ) 16 | 17 | func deleteFile(filepath string) { 18 | if util.CheckExist(filepath) { 19 | if err := os.Remove(filepath); err != nil { 20 | l.Fatal("A error happened when try to delete file", filepath, ":", err) 21 | } 22 | } 23 | } 24 | 25 | func createFile(uploadFile multipart.File, filepath string, taskID string) (*os.File, error) { 26 | // If error happened when change task state, close file 27 | if err := plugins.ChangeTaskState(buildCreatingState(taskID)); err != nil { 28 | _ = uploadFile.Close() 29 | l.Fatal("Error happened when change state of task", taskID, "to copying:", err) 30 | } 31 | l.Debug("Change state of task", taskID, "to creating state successfully") 32 | 33 | saveTo, err := os.Create(filepath) 34 | 35 | if err != nil { 36 | // create file failed, close file and change state 37 | l.Error("Error happened when create file of task", taskID, ":", err) 38 | _ = uploadFile.Close() 39 | 40 | if err := plugins.ChangeTaskState(api.BuildErrorState(taskID, err.Error())); err != nil { 41 | // change task state error, exit. 42 | l.Fatal("A error happened when change task", taskID, "to error state:", err) 43 | } else { 44 | l.Warn("Change task", taskID, "state to error successfully") 45 | } 46 | return nil, err 47 | } 48 | 49 | l.Debug("Create file on fs successfully:", saveTo.Name()) 50 | 51 | return saveTo, nil 52 | } 53 | 54 | func fileCopy(uploadFile multipart.File, saveTo *os.File, filepath string, taskID string) { 55 | // If error happened when change task state, delete file and close 56 | if err := plugins.ChangeTaskState(buildCopyingState(taskID)); err != nil { 57 | _ = uploadFile.Close() 58 | _ = saveTo.Close() 59 | deleteFile(filepath) 60 | l.Fatal("Error happened when change state of task", taskID, "to copying:", err) 61 | } 62 | l.Debug("Change task", taskID, "state to copy successfully") 63 | 64 | // sleep for debug javascript 65 | if *argFsDebugSleep > 0 { 66 | l.Debug("Sleep", *argFsDebugSleep, "ms for debug") 67 | time.Sleep(time.Duration(*argFsDebugSleep) * time.Millisecond) 68 | } 69 | 70 | l.Info("Start copy file of task", taskID) 71 | 72 | // copy file to disk, then close 73 | _, err := io.Copy(saveTo, uploadFile) 74 | _ = saveTo.Close() 75 | _ = uploadFile.Close() 76 | 77 | if err != nil { 78 | // copy file failed, delete file and turn state to error 79 | l.Warn("Error happened when copy file of task", taskID, ":", err) 80 | deleteFile(filepath) 81 | 82 | var err2 error 83 | if err2 = plugins.ChangeTaskState(api.BuildErrorState(taskID, err.Error())); err2 != nil { 84 | l.Fatal("Error happened when change task", taskID, "to error state:", err2) 85 | } else { 86 | l.Warn("Change task", taskID, "state to error successfully") 87 | } 88 | } 89 | 90 | // copy file successfully 91 | l.Info("File copy of task", taskID, "finished") 92 | 93 | // delete successful task, non-exist task means successful 94 | if err := plugins.DeleteTask(taskID); err == nil { 95 | l.Debug("Task", taskID, "finished, deleted it from task list") 96 | } else { 97 | // delete task failed, delete file and exit 98 | deleteFile(filepath) 99 | l.Fatal("A error happened when delete task", taskID, ":", err) 100 | } 101 | } 102 | 103 | // background operate, save file to disk 104 | func saveFile(uploadFile multipart.File, filename string) { 105 | defer func() { 106 | if r := recover(); r != nil { 107 | var errorMsg string 108 | switch t := r.(type) { 109 | case string: 110 | errorMsg = t 111 | case error: 112 | errorMsg = t.Error() 113 | default: 114 | errorMsg = "Unknown Panic" 115 | } 116 | l.Error("Panic happened when do background task:", errorMsg) 117 | } 118 | }() 119 | 120 | filepath := pathUtil.Join(imageDir, filename) 121 | 122 | saveTo, err := createFile(uploadFile, filepath, filename) 123 | if err != nil { 124 | return 125 | } 126 | 127 | fileCopy(uploadFile, saveTo, filepath, filename) 128 | } 129 | 130 | // SaveRequestHandle Will be called when receive a file save request. 131 | func (fsp fsPlugin) SaveRequestHandle(q *plugins.SaveRequest) (*api.TaskId, error) { 132 | l.Debug("Receive a file save request") 133 | 134 | taskID := uuid.NewV4().String() + "." + q.FileExt 135 | 136 | // create task 137 | if plugins.CreateTask(taskID) != nil { 138 | l.Fatal("Error happened when create new task!") 139 | } 140 | l.Debug("create task", taskID, "successfully, starting background task") 141 | 142 | // start background copy operate 143 | go saveFile(q.File, taskID) 144 | 145 | l.Debug("Background task started, return task ID:", taskID) 146 | return &api.TaskId{TaskId: taskID}, nil 147 | } 148 | -------------------------------------------------------------------------------- /plugins/fs/state.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | pathUtil "path/filepath" 5 | 6 | "github.com/7sDream/rikka/api" 7 | "github.com/7sDream/rikka/common/util" 8 | "github.com/7sDream/rikka/plugins" 9 | ) 10 | 11 | const ( 12 | stateCopying = "copying" 13 | stateCopyingCode = 2 14 | stateCopyingDesc = "Image is being copied to rikka file system" 15 | 16 | stateCreating = "creating" 17 | stateCreatingCode = 3 18 | stateCreatingDesc = "Creating file in fs to store your image" 19 | ) 20 | 21 | // A shortcut function to build state we need. 22 | func buildCreatingState(taskID string) *api.State { 23 | return &api.State{ 24 | TaskID: taskID, 25 | StateCode: stateCreatingCode, 26 | State: stateCreating, 27 | Description: stateCreatingDesc, 28 | } 29 | } 30 | 31 | // A shortcut function to build state we need. 32 | func buildCopyingState(taskID string) *api.State { 33 | return &api.State{ 34 | TaskID: taskID, 35 | StateCode: stateCopyingCode, 36 | State: stateCopying, 37 | Description: stateCopyingDesc, 38 | } 39 | } 40 | 41 | // StateRequestHandle Will be called when receive a get state request. 42 | func (fsp fsPlugin) StateRequestHandle(taskID string) (pState *api.State, err error) { 43 | 44 | l.Debug("Receive a state request of taskID", taskID) 45 | 46 | // taskID exist on task list, just return it 47 | if pState, err = plugins.GetTaskState(taskID); err == nil { 48 | if pState.StateCode == api.StateErrorCode { 49 | l.Warn("Get a error state of task", taskID, *pState) 50 | } else { 51 | l.Debug("Get a normal state of task", taskID, *pState) 52 | } 53 | return pState, nil 54 | } 55 | 56 | l.Debug("State of task", taskID, "not found, check if file exist") 57 | // TaskId not exist or error when get it, check if image file already exist 58 | if util.IsFile(pathUtil.Join(imageDir, taskID)) { 59 | // file exist is regarded as a finished state 60 | pFinishState := api.BuildFinishState(taskID) 61 | l.Debug("File of task", taskID, "exist, return finished state", *pFinishState) 62 | return pFinishState, nil 63 | } 64 | 65 | l.Warn("File of task", taskID, "not exist, get state error:", err) 66 | // get state error 67 | return nil, err 68 | } 69 | -------------------------------------------------------------------------------- /plugins/fs/url.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/url" 7 | pathUtil "path/filepath" 8 | 9 | "github.com/7sDream/rikka/api" 10 | "github.com/7sDream/rikka/common/util" 11 | "github.com/7sDream/rikka/plugins" 12 | ) 13 | 14 | // buildURL build complete url from request's Host header and task ID 15 | func buildURL(r *http.Request, scheme string, taskID string) string { 16 | res := url.URL{ 17 | Scheme: scheme, 18 | Host: r.Host, 19 | // remove root / 20 | Path: fileURLPath[1:] + taskID, 21 | } 22 | return res.String() 23 | } 24 | 25 | // URLRequestHandle will be called when receive a get image url by taskID request 26 | func (fsp fsPlugin) URLRequestHandle(q *plugins.URLRequest) (pURL *api.URL, err error) { 27 | taskID := q.TaskID 28 | r := q.HTTPRequest 29 | 30 | l.Debug("Receive an url request of task", taskID) 31 | l.Debug("Check if file exist of task", taskID) 32 | // If file exist, return url 33 | if util.CheckExist(pathUtil.Join(imageDir, taskID)) { 34 | scheme := "http" 35 | if q.IsServeTLS { 36 | scheme += "s" 37 | } 38 | taskUrl := buildURL(r, scheme, taskID) 39 | l.Debug("File of task", taskID, "exist, return taskUrl", taskUrl) 40 | return &api.URL{URL: taskUrl}, nil 41 | } 42 | l.Error("File of task", taskID, "not exist, return error") 43 | return nil, errors.New("file not exist") 44 | } 45 | -------------------------------------------------------------------------------- /plugins/manager.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/7sDream/rikka/api" 9 | "github.com/7sDream/rikka/common/logger" 10 | ) 11 | 12 | var ( 13 | l = logger.NewLogger("[Plugins]") 14 | 15 | currentPlugin RikkaPlugin 16 | ) 17 | 18 | // SubLogger return a new sub logger from plugins logger. 19 | func SubLogger(prefix string) *logger.Logger { 20 | return l.SubLogger(prefix) 21 | } 22 | 23 | // Load load a plugin to net/http 24 | func Load(plugin RikkaPlugin) { 25 | currentPlugin = plugin 26 | currentPlugin.Init() 27 | for _, hp := range currentPlugin.ExtraHandlers() { 28 | http.Handle(hp.Pattern, hp.Handler) 29 | } 30 | } 31 | 32 | // AcceptFile will be called when you receive a file upload request, the SaveRequest struct contains the file. 33 | func AcceptFile(q *SaveRequest) (fileID *api.TaskId, err error) { 34 | return currentPlugin.SaveRequestHandle(q) 35 | } 36 | 37 | // GetState will be called when API server receive a state request. 38 | // Also be called when web server receive a view request, 39 | // web server decide response a finished view html or a self-renewal html based on 40 | // the return state is finished state. 41 | func GetState(taskID string) (r *api.State, err error) { 42 | return currentPlugin.StateRequestHandle(taskID) 43 | } 44 | 45 | // GetURL will be called when API server receive a url request. 46 | // Also be called when web server receive a view request and GetState return a finished state. 47 | // web server use the return url value to render a finished view html. 48 | func GetURL(taskID string, r *http.Request, isServeTLS bool, picOp *ImageOperate) (pURL *api.URL, err error) { 49 | l.Debug("Send state request to plugin before get url of task", taskID) 50 | var pState *api.State 51 | 52 | // check state successfully 53 | if pState, err = GetState(taskID); err == nil { 54 | // not finished 55 | if pState.StateCode != api.StateFinishCode { 56 | l.Warn("Task", taskID, "not finished, can't get url") 57 | return nil, errors.New(api.TaskNotFinishErrMsg) 58 | } 59 | // finished 60 | l.Debug("Task", taskID, "is finished, send url request to the plugin") 61 | return currentPlugin.URLRequestHandle(&URLRequest{ 62 | HTTPRequest: r, 63 | TaskID: taskID, 64 | PicOp: picOp, 65 | IsServeTLS: isServeTLS, 66 | }) 67 | } 68 | 69 | // check state error 70 | errorMsg := fmt.Sprint("Error happened when get state of task", taskID, ":", err) 71 | l.Error(errorMsg) 72 | return nil, errors.New(errorMsg) 73 | } 74 | -------------------------------------------------------------------------------- /plugins/qiniu/README.md: -------------------------------------------------------------------------------- 1 | # Qiniu Plugin 2 | 3 | [中文版][version-zh] 4 | 5 | Added in version 0.2.0. Inner name `qiniu`. 6 | 7 | ## Description 8 | 9 | This plugin use Qiniu Cloud CND to store your image. 10 | 11 | ## Options 12 | 13 | You should provide Qiniu ACCESS KEY, SECRET KEY, bucket name and bucket host. 14 | 15 | ACCESS KEY and SECRET KEY should be add into your env variable, use key `RIKKA_QINIU_ACCESS` and `RIKKA_QINIU_SECRET`. 16 | 17 | Bucket name and bucket host should be provide use command line option: 18 | 19 | `-bname` for the bucket name. 20 | 21 | `-bhost` for bucket host. 22 | 23 | BTW: you can set upload dir by provide `-bpath` option. 24 | 25 | For example,ues `-bpath rikka`, then images will be under `rikka` folder。 26 | 27 | Multi-level dir like `-bpath rikka/images` are also supported. 28 | 29 | ## Guide 30 | 31 | See [Rikka Deploy Guide with Qiniu Plugin on DaoCloud][qiniu-plugin-guide]. 32 | 33 | [version-zh]: https://github.com/7sDream/rikka/blob/master/plugins/qiniu/README.zh.md 34 | [qiniu-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E4%B8%83%E7%89%9B%E4%BA%91%E6%8F%92%E4%BB%B6 35 | -------------------------------------------------------------------------------- /plugins/qiniu/README.zh.md: -------------------------------------------------------------------------------- 1 | # Qiniu 插件 2 | 3 | [English version][version-en] 4 | 5 | 0.2.0 版本添加。内部名 `qiniu`。 6 | 7 | ## 介绍 8 | 9 | 这个插件使用七牛云 CND 来储存你上传的图片。 10 | 11 | ## 参数 12 | 13 | 你需要提供四个参数:七牛的 `ACCESSKEY`, `SECRETKEY`, 以及图片要保存到的空间名和空间域名。 14 | 15 | `ACCESSKEY` 和 `SECRETKEY` 使用环境变量的形式提供,变量名 `RIKKA_QINIU_ACCESS` 和 `RIKKA_QINIU_SECRET`。 16 | 17 | 空间名和空间域名则通过命令行参数提供: 18 | 19 | `-bname` 空间名 20 | 21 | `-bhost` 空间域名 22 | 23 | 另外,你还可以通过提供 `bpath` 参数的形式设置图片需要保存到的文件夹。 24 | 25 | 比如,使用 `-bpath rikka`,上传到的文件会传到空间的 `rikka` 文件夹下。当然 `-bpath rikka/images` 之类的多级文件夹也是可以的。 26 | 27 | ## 部署教程 28 | 29 | 请看部署教程:[在 DaoCloud 上部署使用七牛云插件的 Rikka][qiniu-plugin-guide]。 30 | 31 | [version-en]: https://github.com/7sDream/rikka/blob/master/plugins/qiniu/README.md 32 | [qiniu-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E4%B8%83%E7%89%9B%E4%BA%91%E6%8F%92%E4%BB%B6 33 | -------------------------------------------------------------------------------- /plugins/qiniu/init.go: -------------------------------------------------------------------------------- 1 | package qiniu 2 | 3 | import ( 4 | "github.com/7sDream/rikka/common/util" 5 | "github.com/7sDream/rikka/plugins" 6 | "github.com/qiniu/go-sdk/v7/auth/qbox" 7 | ) 8 | 9 | // Init is the plugin init function, will be called when plugin be load. 10 | func (qnp qiniuPlugin) Init() { 11 | l.Info("Start plugin qiniu") 12 | 13 | plugins.CheckCommonArgs(true, true) 14 | 15 | access = util.GetEnvWithCheck("Access", accessEnvKey, l) 16 | secret = util.GetEnvWithCheck("Secret", secretEnvKey, l) 17 | bucketName = plugins.GetBucketName() 18 | bucketAddr = plugins.GetBucketHost() 19 | bucketPrefix = plugins.GetBucketPath() 20 | 21 | mac = qbox.NewMac(access, secret) 22 | 23 | l.Info("Qiniu plugin start successfully") 24 | } 25 | -------------------------------------------------------------------------------- /plugins/qiniu/qiniu.go: -------------------------------------------------------------------------------- 1 | package qiniu 2 | 3 | import ( 4 | "github.com/7sDream/rikka/plugins" 5 | "github.com/qiniu/go-sdk/v7/auth/qbox" 6 | "github.com/qiniu/go-sdk/v7/storage" 7 | ) 8 | 9 | // plugin type 10 | type qiniuPlugin struct{} 11 | 12 | const ( 13 | accessEnvKey = "RIKKA_QINIU_ACCESS" 14 | secretEnvKey = "RIKKA_QINIU_SECRET" 15 | ) 16 | 17 | var ( 18 | l = plugins.SubLogger("[Qiniu]") 19 | 20 | access string 21 | secret string 22 | bucketName string 23 | bucketAddr string 24 | bucketPrefix string 25 | 26 | conf = &storage.Config{ 27 | Zone: &storage.ZoneHuadong, 28 | UseHTTPS: true, 29 | UseCdnDomains: true, 30 | } 31 | mac *qbox.Mac 32 | 33 | // Plugin is the main plugin instance 34 | Plugin = qiniuPlugin{} 35 | ) 36 | 37 | func (qnp qiniuPlugin) ExtraHandlers() []plugins.HandlerWithPattern { 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /plugins/qiniu/save.go: -------------------------------------------------------------------------------- 1 | package qiniu 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/7sDream/rikka/api" 7 | "github.com/7sDream/rikka/plugins" 8 | "github.com/qiniu/go-sdk/v7/storage" 9 | "github.com/satori/go.uuid" 10 | ) 11 | 12 | type putRet struct { 13 | Hash string `json:"hash"` 14 | Key string `json:"key"` 15 | } 16 | 17 | func buildPath(taskID string) string { 18 | return bucketPrefix + taskID 19 | } 20 | 21 | func uploadToQiniu(taskID string, q *plugins.SaveRequest) { 22 | l.Debug("Getting upload token of task", taskID) 23 | 24 | // preparing... 25 | err := plugins.ChangeTaskState(buildPreparingState(taskID)) 26 | if err != nil { 27 | l.Fatal("Error happened when change state of task", taskID, "to preparing:", err) 28 | } 29 | l.Debug("Change state of task", taskID, "to preparing successfully") 30 | 31 | policy := storage.PutPolicy{ 32 | Scope: bucketName, 33 | } 34 | upToken := policy.UploadToken(mac) 35 | 36 | uploader := storage.NewFormUploader(conf) 37 | 38 | // uploading 39 | l.Debug("Upload with arg", "key:", buildPath(taskID), ", file size:", q.FileSize) 40 | err = plugins.ChangeTaskState(buildUploadingState(taskID)) 41 | if err != nil { 42 | l.Fatal("Error happened when change state of task", taskID, "to uploading:", err) 43 | } 44 | l.Debug("Change state of task", taskID, "to uploading successfully") 45 | 46 | var ret putRet 47 | err = uploader.Put(context.Background(), &ret, upToken, buildPath(taskID), q.File, q.FileSize, nil) 48 | 49 | // uploading error 50 | if err != nil { 51 | l.Error("Error happened when upload task", taskID, ":", err) 52 | err = plugins.ChangeTaskState(api.BuildErrorState(taskID, err.Error())) 53 | if err != nil { 54 | l.Fatal("Error happened when change state of task", taskID, "to error:", err) 55 | } 56 | l.Debug("Change state of task", taskID, "to error successfully") 57 | } else { 58 | // uploading successfully 59 | l.Info("Upload task", taskID, "to qiniu cloud successfully") 60 | err = plugins.DeleteTask(taskID) 61 | if err != nil { 62 | l.Fatal("Error happened when delete state of task", taskID, ":", err) 63 | } 64 | l.Debug("Delete task", taskID, "successfully") 65 | } 66 | } 67 | 68 | func (qnp qiniuPlugin) SaveRequestHandle(q *plugins.SaveRequest) (*api.TaskId, error) { 69 | l.Debug("Receive a file save request") 70 | 71 | taskID := uuid.NewV4().String() + "." + q.FileExt 72 | 73 | err := plugins.CreateTask(taskID) 74 | if err != nil { 75 | l.Fatal("Error happened when create new task!") 76 | } 77 | l.Debug("create task", taskID, "successfully, starting background task") 78 | 79 | go uploadToQiniu(taskID, q) 80 | 81 | l.Debug("Background task started, return task ID:", taskID) 82 | return &api.TaskId{TaskId: taskID}, nil 83 | } 84 | -------------------------------------------------------------------------------- /plugins/qiniu/state.go: -------------------------------------------------------------------------------- 1 | package qiniu 2 | 3 | import ( 4 | "github.com/7sDream/rikka/api" 5 | "github.com/7sDream/rikka/plugins" 6 | ) 7 | 8 | const ( 9 | statePreparing = "preparing" 10 | statePreparingCode = 2 11 | statePreparingDesc = "Rikka is preparing to upload your image to Qiniu cloud" 12 | 13 | stateUploading = "uploading" 14 | stateUploadingCode = 3 15 | stateUploadingDesc = "Rikka is uploading your image to Qiniu cloud" 16 | ) 17 | 18 | func buildPreparingState(taskID string) *api.State { 19 | return &api.State{ 20 | TaskID: taskID, 21 | State: statePreparing, 22 | StateCode: statePreparingCode, 23 | Description: statePreparingDesc, 24 | } 25 | } 26 | 27 | func buildUploadingState(taskID string) *api.State { 28 | return &api.State{ 29 | TaskID: taskID, 30 | State: stateUploading, 31 | StateCode: stateUploadingCode, 32 | Description: stateUploadingDesc, 33 | } 34 | } 35 | 36 | func (qnp qiniuPlugin) StateRequestHandle(taskID string) (pState *api.State, err error) { 37 | l.Debug("Receive a state request of taskID", taskID) 38 | 39 | pState, err = plugins.GetTaskState(taskID) 40 | if err != nil { 41 | l.Debug("State of task", taskID, "not found, just return a finish state") 42 | return api.BuildFinishState(taskID), nil 43 | } 44 | 45 | if pState.StateCode == api.StateErrorCode { 46 | l.Warn("Get a error state of task", taskID, *pState) 47 | } else { 48 | l.Debug("Get a normal state of task", taskID, *pState) 49 | } 50 | return pState, nil 51 | } 52 | -------------------------------------------------------------------------------- /plugins/qiniu/url.go: -------------------------------------------------------------------------------- 1 | package qiniu 2 | 3 | import ( 4 | "github.com/7sDream/rikka/api" 5 | "github.com/7sDream/rikka/plugins" 6 | ) 7 | 8 | func buildURL(taskID string) string { 9 | return bucketAddr + "/" + buildPath(taskID) 10 | } 11 | 12 | // URLRequestHandle will be called when receive a get image url by taskID request 13 | func (qnp qiniuPlugin) URLRequestHandle(q *plugins.URLRequest) (pURL *api.URL, err error) { 14 | l.Debug("Receive an url request of task", q.TaskID) 15 | return &api.URL{URL: buildURL(q.TaskID)}, nil 16 | } 17 | -------------------------------------------------------------------------------- /plugins/task.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/7sDream/rikka/api" 8 | ) 9 | 10 | var ( 11 | tasks = struct { 12 | sync.RWMutex 13 | m map[string]*api.State 14 | }{ 15 | m: make(map[string]*api.State), 16 | } 17 | ) 18 | 19 | // CreateTask add a task to task list. 20 | // If taskID already exist, return an error. 21 | func CreateTask(taskID string) error { 22 | tasks.Lock() 23 | defer tasks.Unlock() 24 | 25 | if _, ok := tasks.m[taskID]; ok { // key exist 26 | return errors.New(api.TaskAlreadyExistErrMsg) 27 | } 28 | 29 | tasks.m[taskID] = api.BuildCreateState(taskID) 30 | return nil 31 | } 32 | 33 | // ChangeTaskState change the state of a task. 34 | // If taskID not exist, return an error. 35 | func ChangeTaskState(pProvidedState *api.State) error { 36 | tasks.Lock() 37 | defer tasks.Unlock() 38 | 39 | if pState, ok := tasks.m[pProvidedState.TaskID]; ok { // key exist 40 | pState.StateCode = pProvidedState.StateCode 41 | pState.State = pProvidedState.State 42 | pState.Description = pProvidedState.Description 43 | return nil 44 | } 45 | 46 | return errors.New(api.TaskNotExistErrMsg) 47 | } 48 | 49 | // GetTaskState get state of a task. 50 | // If task not exist, return an error. 51 | func GetTaskState(taskID string) (*api.State, error) { 52 | tasks.RLock() 53 | defer tasks.RUnlock() 54 | 55 | if pState, ok := tasks.m[taskID]; ok { // key exist 56 | return pState, nil 57 | } 58 | return nil, errors.New(api.TaskNotExistErrMsg) 59 | } 60 | 61 | // DeleteTask delete a task from task list. 62 | // If taskID not exist, return an error. 63 | func DeleteTask(taskID string) error { 64 | tasks.Lock() 65 | defer tasks.Unlock() 66 | 67 | if _, ok := tasks.m[taskID]; ok { // key exist 68 | delete(tasks.m, taskID) 69 | return nil 70 | } 71 | 72 | return errors.New(api.TaskNotExistErrMsg) 73 | } 74 | -------------------------------------------------------------------------------- /plugins/tencent/ci/README.md: -------------------------------------------------------------------------------- 1 | # Tencent CI —— Cloud Image Plugin 2 | 3 | [中文版][version-zh] 4 | 5 | Added in version 0.4.0, inner name `tcci`. 6 | 7 | ## Description 8 | 9 | This plugin use Cloud Image (CI) of Tencent to store image files. 10 | 11 | ## Options 12 | 13 | You should provide 4 options: APPID, Secret ID, Secret Key and Bucket Name. 14 | 15 | First three options should be provided in env var, use key `RIKKA_TENCENT_APPID`, `RIKKA_TENCENT_SECRETID` and `RIKKA_TENCENT_SECRETKEY`. 16 | 17 | And the Bucket Name should be specified by the command line option `-bname`. 18 | 19 | If you want, you can use option `-bpath` to set the path image will be store to. 20 | 21 | For example, `-bpath rikka`,will save image in `rikka` folder. 22 | 23 | ## Guide 24 | 25 | See [Rikka Deploy Guide with TC-CI Plugin on DaoCloud][tcci-plugin-guide]. 26 | 27 | [version-zh]: https://github.com/7sDream/rikka/blob/master/plugins/tencent/ci/README.zh.md 28 | [tcci-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E8%85%BE%E8%AE%AF-CI-%E6%8F%92%E4%BB%B6 29 | -------------------------------------------------------------------------------- /plugins/tencent/ci/README.zh.md: -------------------------------------------------------------------------------- 1 | # TC-CI 腾讯万象优图 Cloud Image 插件 2 | 3 | [English version][version-en] 4 | 5 | 0.4.0 版本添加,内部名 `tcci`。 6 | 7 | ## 简介 8 | 9 | 这个插件使用腾讯云的万象优图(Cloud Image, CI)来储存图片。 10 | 11 | ## 参数 12 | 13 | 你需要提供四个参数:腾讯云的项目编号(APPID),密钥ID(SecretID),密钥Key(SecretKey)以及储存空间名(bucket name)。 14 | 15 | 前三个通过环境变量提供,分别为 `RIKKA_TENCENT_APPID`, `RIKKA_TENCENT_SECRETID`, `RIKKA_TENCENT_SECRETKEY`。 16 | 17 | 储存空间名通过命令行参数 `-bname` 提供。 18 | 19 | 另外,你还可以通过提供 `bpath` 参数的形式设置图片需要保存到的文件夹。 20 | 21 | 比如,使用 `-bpath rikka`,上传到的文件会传到空间的 `rikka` 文件夹下。 22 | 23 | ## 部署教程 24 | 25 | 请看部署教程:[在 DaoCloud 上部署使用 TC-CI 插件的 Rikka][tcci-plugin-guide] 26 | 27 | [version-en]: https://github.com/7sDream/rikka/blob/master/plugins/tencent/ci/README.md 28 | [tcci-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E8%85%BE%E8%AE%AF-CI-%E6%8F%92%E4%BB%B6 29 | -------------------------------------------------------------------------------- /plugins/tencent/ci/ci.go: -------------------------------------------------------------------------------- 1 | package ci 2 | 3 | import ( 4 | "github.com/7sDream/rikka/plugins" 5 | qcloud "github.com/jeremywangjun/image-go-sdk" 6 | ) 7 | 8 | type TencentCloudImagePlugin struct{} 9 | 10 | var ( 11 | l = plugins.SubLogger("[TC-CI]") 12 | 13 | appID string 14 | secretID string 15 | secretKey string 16 | bucketName string 17 | bucketHost string 18 | bucketPath string 19 | 20 | // Plugin is the main plugin instance 21 | Plugin TencentCloudImagePlugin 22 | 23 | cloud *qcloud.PicCloud 24 | ) 25 | 26 | func buildFullPath(taskID string) string { 27 | return bucketPath + taskID 28 | } 29 | 30 | func (plugin TencentCloudImagePlugin) ExtraHandlers() []plugins.HandlerWithPattern { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /plugins/tencent/ci/init.go: -------------------------------------------------------------------------------- 1 | package ci 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/7sDream/rikka/plugins" 7 | "github.com/7sDream/rikka/plugins/tencent" 8 | qcloud "github.com/jeremywangjun/image-go-sdk" 9 | ) 10 | 11 | func (plugin TencentCloudImagePlugin) Init() { 12 | l.Info("Start plugin tencent cloud image") 13 | 14 | plugins.CheckCommonArgs(true, false) 15 | 16 | appID = tencent.GetAppIDWithCheck(l) 17 | secretID = tencent.GetSecretIDWithCheck(l) 18 | secretKey = tencent.GetSecretKeyWithCheck(l) 19 | bucketName = plugins.GetBucketName() 20 | bucketPath = plugins.GetBucketPath() 21 | 22 | appIDUint, err := strconv.Atoi(appID) 23 | if err != nil { 24 | l.Fatal("Error happened when parse APPID to int:", err) 25 | } 26 | l.Debug("Parse APPID to int successfully") 27 | 28 | cloud = &qcloud.PicCloud{ 29 | Appid: uint(appIDUint), 30 | SecretId: secretID, 31 | SecretKey: secretKey, 32 | Bucket: bucketName, 33 | } 34 | 35 | l.Info("Tencent cloud image plugin start successfully") 36 | } 37 | -------------------------------------------------------------------------------- /plugins/tencent/ci/save.go: -------------------------------------------------------------------------------- 1 | package ci 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | 7 | "github.com/7sDream/rikka/api" 8 | "github.com/7sDream/rikka/plugins" 9 | "github.com/satori/go.uuid" 10 | ) 11 | 12 | const ( 13 | taskIDPlaceholder = "{TaskId}" 14 | ) 15 | 16 | func uploadToCI(q *plugins.SaveRequest, taskID string) { 17 | defer func() { 18 | if err := recover(); err != nil { 19 | l.Error("Panic happened in background:", err) 20 | var errorMsg string 21 | switch t := err.(type) { 22 | case string: 23 | errorMsg = t 24 | case error: 25 | errorMsg = t.Error() 26 | default: 27 | errorMsg = "Unknown" 28 | } 29 | _ = plugins.ChangeTaskState(api.BuildErrorState(taskID, errorMsg)) 30 | } 31 | }() 32 | 33 | err := plugins.ChangeTaskState(buildReadingState(taskID)) 34 | if err != nil { 35 | l.Fatal("Error happened when change state of task", taskID, "to reading file:", err) 36 | } 37 | l.Debug("Change state of task", taskID, "to reading file successfully") 38 | 39 | fileContent, err := ioutil.ReadAll(q.File) 40 | 41 | //noinspection GoUnhandledErrorResult 42 | defer q.File.Close() 43 | 44 | if err != nil { 45 | l.Error("Error happened when reading uploaded file of task", taskID, ":", err) 46 | err := plugins.ChangeTaskState(api.BuildErrorState(taskID, err.Error())) 47 | if err != nil { 48 | l.Fatal("Error happened when change state of task", taskID, "to error:", err) 49 | } 50 | l.Debug("Change state of task", taskID, "to error successfully") 51 | return 52 | } 53 | l.Debug("Read file of task", taskID, "successfully") 54 | 55 | err = plugins.ChangeTaskState(buildUploadingState(taskID)) 56 | if err != nil { 57 | l.Fatal("Error happened when change state of task", taskID, "to uploading file:", err) 58 | } 59 | l.Debug("Change state of task", taskID, "to uploading file successfully") 60 | 61 | info, err := cloud.UploadWithFileid(fileContent, buildFullPath(taskID)) 62 | if err != nil { 63 | l.Error("Error happened when uploading file of task to ci", taskID, ":", err) 64 | err := plugins.ChangeTaskState(api.BuildErrorState(taskID, err.Error())) 65 | if err != nil { 66 | l.Fatal("Error happened when change state of task", taskID, "to error:", err) 67 | } 68 | l.Debug("Change state of task", taskID, "to error successfully") 69 | return 70 | } 71 | l.Debug("Uploading file", taskID, "successfully") 72 | 73 | if err := plugins.DeleteTask(taskID); err != nil { 74 | l.Error("Error happened when delete task", taskID, ":", err) 75 | } 76 | l.Debug("Delete task", taskID, "successfully") 77 | 78 | if bucketHost == "" { 79 | bucketHost = strings.Replace(info.DownloadUrl, taskID, taskIDPlaceholder, -1) 80 | l.Debug("Get image url format:", bucketHost) 81 | } 82 | } 83 | 84 | func (plugin TencentCloudImagePlugin) SaveRequestHandle(q *plugins.SaveRequest) (*api.TaskId, error) { 85 | l.Debug("Receive a file save request") 86 | taskID := uuid.NewV4().String() + "." + q.FileExt 87 | 88 | err := plugins.CreateTask(taskID) 89 | if err != nil { 90 | l.Fatal("Error happened when create new task!") 91 | } 92 | l.Debug("create task", taskID, "successfully, starting background task") 93 | 94 | go uploadToCI(q, taskID) 95 | 96 | l.Debug("Background task started, return task ID:", taskID) 97 | return &api.TaskId{TaskId: taskID}, nil 98 | } 99 | -------------------------------------------------------------------------------- /plugins/tencent/ci/state.go: -------------------------------------------------------------------------------- 1 | package ci 2 | 3 | import ( 4 | "github.com/7sDream/rikka/api" 5 | "github.com/7sDream/rikka/plugins" 6 | ) 7 | 8 | const ( 9 | stateReading = "reading file" 10 | stateReadingCode = 2 11 | stateReadingDesc = "Rikka is reading your image file" 12 | 13 | stateUploading = "uploading" 14 | stateUploadingCode = 3 15 | stateUploadingDesc = "Rikka is uploading your image to Tencent CI" 16 | ) 17 | 18 | func buildUploadingState(taskID string) *api.State { 19 | return &api.State{ 20 | TaskID: taskID, 21 | State: stateUploading, 22 | StateCode: stateUploadingCode, 23 | Description: stateUploadingDesc, 24 | } 25 | } 26 | 27 | func buildReadingState(taskID string) *api.State { 28 | return &api.State{ 29 | TaskID: taskID, 30 | State: stateReading, 31 | StateCode: stateReadingCode, 32 | Description: stateReadingDesc, 33 | } 34 | } 35 | 36 | func (plugin TencentCloudImagePlugin) StateRequestHandle(taskID string) (*api.State, error) { 37 | pState, err := plugins.GetTaskState(taskID) 38 | if err != nil { 39 | // Not exist as finished 40 | return api.BuildFinishState(taskID), nil 41 | } 42 | return pState, nil 43 | } 44 | -------------------------------------------------------------------------------- /plugins/tencent/ci/url.go: -------------------------------------------------------------------------------- 1 | package ci 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/7sDream/rikka/api" 8 | "github.com/7sDream/rikka/plugins" 9 | ) 10 | 11 | func (plugin TencentCloudImagePlugin) URLRequestHandle(q *plugins.URLRequest) (*api.URL, error) { 12 | if bucketHost == "" { 13 | l.Error("Request URL of task", q.TaskID, "before state become to finish") 14 | return nil, errors.New("get url before task finish") 15 | } 16 | 17 | if !strings.Contains(bucketHost, taskIDPlaceholder) { 18 | l.Fatal("Error! Unable to get image url from bucket host:", bucketHost) 19 | } 20 | 21 | return &api.URL{ 22 | URL: strings.Replace(bucketHost, taskIDPlaceholder, q.TaskID, -1), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /plugins/tencent/cos/README.md: -------------------------------------------------------------------------------- 1 | # Tencent COS —— Cloud Object Service Plugin 2 | 3 | [中文版][version-zh] 4 | 5 | Added in version 0.4.0, inner name `tccos`. 6 | 7 | ## Description 8 | 9 | This plugin use Cloud Object Service (COS) of Tencent to store image files. 10 | 11 | ## Options 12 | 13 | You should provide 4 options: APPID, Secret ID, Secret Key and Bucket Name. 14 | 15 | First three options should be provided in env var, use key `RIKKA_TENCENT_APPID`, `RIKKA_TENCENT_SECRETID` and `RIKKA_TENCENT_SECRETKEY`. 16 | 17 | And the Bucket Name should be specified by the command line option `-bname`. 18 | 19 | If you want, you can use option `-bpath` to set the path image will be store to(Notice: The path should already exist). 20 | 21 | For example, `-bpath rikka`,will save image in `rikka` folder. 22 | 23 | The object storage version is provided by the command line parameter `-tccosVer`. The default version is `v4`, for keep compatibility. 24 | 25 | If you need to use `v5` version, then have to set the region in the domain name by add the env var `RIKKA_TENCENT_REGION`. 26 | 27 | ## Notices 28 | 29 | As a "Static File Store Server", Tencent COS make browsers download the file when visits file url, instead of preview them. 30 | 31 | In other word, if you visit a url of image saved in COS in your browser, it will download the image rather than open a new tab to preview the image. 32 | 33 | But it's ok to use the image url in `src` attr of `image` element, or `background` attr of other elements in HTML, 34 | 35 | If you want change this action, you need to bind COS to your own domain and enable the static website option. 36 | 37 | Refer: [Tencent COS static website option doc][tencent-cos-static-website-doc]. 38 | 39 | ## Guide 40 | 41 | See [Rikka Deploy Guide with TC-COS Plugin on DaoCloud][tccos-plugin-guide]. 42 | 43 | [version-zh]: https://github.com/7sDream/rikka/blob/master/plugins/tencent/cos/README.zh.md 44 | [tencent-cos-static-website-doc]: https://www.qcloud.com/doc/product/227/%E9%85%8D%E7%BD%AE%E8%AF%A6%E6%83%85#5-静态网站 45 | [tccos-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E8%85%BE%E8%AE%AF-COS-%E6%8F%92%E4%BB%B6 46 | -------------------------------------------------------------------------------- /plugins/tencent/cos/README.zh.md: -------------------------------------------------------------------------------- 1 | # TC-COS 腾讯对象储存服务 Cloud Object Service 插件 2 | 3 | [English version][version-en] 4 | 5 | 0.4.0 版本添加,内部名 `tccos`。 6 | 7 | ## 简介 8 | 9 | 这个插件使用腾讯云的对象储存服务(Cloud Object Service, COS)来储存图片。 10 | 11 | ## 参数 12 | 13 | 你需要提供四个参数:腾讯云的项目编号(APPID),密钥ID(SecretID),密钥Key(SecretKey)以及储存空间名(bucket name)。 14 | 15 | 前三个通过环境变量提供,分别为 `RIKKA_TENCENT_APPID`, `RIKKA_TENCENT_SECRETID`, `RIKKA_TENCENT_SECRETKEY`。 16 | 17 | 储存空间名通过命令行参数 `-bname` 提供。 18 | 19 | 另外,你还可以通过提供 `bpath` 参数的形式设置图片需要保存到的文件夹。(注意,文件夹必须在 COS 里已经存在) 20 | 21 | 比如,使用 `-bpath rikka`,上传到的文件会传到空间的 `rikka` 文件夹下。 22 | 23 | 对象存储版本通过命令行参数 `-tccosVer` 提供,为保持老版本兼容性,默认为 `v4`。 24 | 25 | 如需使用 `v5` 版本需要通过添加环境变量 `RIKKA_TENCENT_REGION` 来设置域名中的所属地域(region)。 26 | 27 | ## 注意事项 28 | 29 | 腾讯 COS 作为「文件储存服务」,其默认对于存放的文件提供的是下载服务而非预览服务。 30 | 31 | 也就是说如果你把一个储存在 COS 上的图片链接直接用浏览器打开,浏览器会执行下载动作而不是在标签页里预览图片。 32 | 33 | 但是放在 HTML 的 `img` 元素的 `src` 属性里,或者其他元素的 `background` 属性里作为图片显示是没有问题的。 34 | 35 | 如果想打开链接时触发图片预览,请在 COS 设置页面绑定自定义域名并打开静态网站选项。 36 | 37 | 参考:[腾讯云 COS 静态网站选项][tencent-cos-static-website-doc]。 38 | 39 | ## 部署教程 40 | 41 | 请看部署教程:[在 DaoCloud 上部署使用 TC-COS 插件的 Rikka][tccos-plugin-guide] 42 | 43 | [version-en]: https://github.com/7sDream/rikka/blob/master/plugins/tencent/cos/README.md 44 | [tencent-cos-static-website-doc]: https://www.qcloud.com/doc/product/227/%E9%85%8D%E7%BD%AE%E8%AF%A6%E6%83%85#5-静态网站 45 | [tccos-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E8%85%BE%E8%AE%AF-COS-%E6%8F%92%E4%BB%B6 46 | -------------------------------------------------------------------------------- /plugins/tencent/cos/client.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/hmac" 7 | "crypto/sha1" 8 | "encoding/base64" 9 | "encoding/hex" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io/ioutil" 14 | "math/rand" 15 | "mime/multipart" 16 | "net/http" 17 | "net/url" 18 | "strings" 19 | "time" 20 | 21 | "github.com/tencentyun/cos-go-sdk-v5" 22 | 23 | "github.com/7sDream/rikka/plugins" 24 | ) 25 | 26 | const ( 27 | uploadBaseURL = "http://web.file.myqcloud.com/files/v1/%s/%s/%s%s" 28 | taskIDPlaceholder = "{taskID}" 29 | baseCosV5Url = "http://%s-%s.cos.%s.myqcloud.com" 30 | ) 31 | 32 | type cosClient struct { 33 | http.Client 34 | sign string 35 | expire time.Time 36 | } 37 | 38 | type cosSdkV5Client struct { 39 | *cos.Client 40 | } 41 | 42 | type genericCosClient interface { 43 | Upload(q *plugins.SaveRequest, taskID string) error 44 | } 45 | 46 | func makeSign(current *time.Time, dur *time.Duration, randInt *int) (string, time.Time) { 47 | var t time.Time 48 | if current == nil { 49 | t = time.Now().UTC() 50 | } else { 51 | t = *current 52 | } 53 | if dur == nil { 54 | durObj := 90 * 24 * time.Hour 55 | dur = &durObj 56 | } 57 | e := t.Add(*dur) 58 | if randInt == nil { 59 | number := rand.Intn(10000000000) 60 | randInt = &number 61 | } 62 | // Original = "a=[appid]&b=[bucket]&k=[SecretID]&e=[expiredTime]&t=[currentTime]&r=[rand]&f=" 63 | original := fmt.Sprintf( 64 | "a=%s&b=%s&k=%s&e=%d&t=%d&r=%d&f=", 65 | appID, bucketName, secretID, e.Unix(), // 60 * 60 * 24 * 90 = 90 days 66 | t.Unix(), *randInt, // random integer, max length: 10 67 | ) 68 | hmacBuilder := hmac.New(sha1.New, []byte(secretKey)) 69 | hmacBuilder.Write([]byte(original)) 70 | signTemp := hmacBuilder.Sum(nil) 71 | sign := base64.StdEncoding.EncodeToString(append(signTemp, []byte(original)...)) 72 | return sign, e 73 | } 74 | 75 | func newCosClient() *cosClient { 76 | sign, expire := makeSign(nil, nil, nil) 77 | return &cosClient{ 78 | Client: http.Client{}, 79 | sign: sign, 80 | expire: expire, 81 | } 82 | } 83 | 84 | func newCosSdkV5Client() *cosSdkV5Client { 85 | rawUrl := fmt.Sprintf(baseCosV5Url, bucketName, appID, region) 86 | u, _ := url.Parse(rawUrl) 87 | return &cosSdkV5Client{ 88 | cos.NewClient(&cos.BaseURL{BucketURL: u}, &http.Client{ 89 | Transport: &cos.AuthorizationTransport{ 90 | SecretID: secretID, 91 | SecretKey: secretKey, 92 | }, 93 | }), 94 | } 95 | } 96 | 97 | func (c *cosClient) auxMakeUploadRequest(q *plugins.SaveRequest, taskID string) (*http.Request, error) { 98 | body := &bytes.Buffer{} 99 | writer := multipart.NewWriter(body) 100 | part, err := writer.CreateFormFile("fileContent", taskID) 101 | if err != nil { 102 | l.Error("Error happened when create form file of task", taskID, ":", err) 103 | return nil, err 104 | } 105 | l.Debug("Create form writer of task", taskID, "successfully") 106 | 107 | fileContent, err := ioutil.ReadAll(q.File) 108 | 109 | //noinspection GoUnhandledErrorResult 110 | defer q.File.Close() 111 | 112 | if err != nil { 113 | l.Error("Error happened when read file content of task", taskID, ":", err) 114 | return nil, err 115 | } 116 | l.Debug("Read file content of task", taskID, "successfully") 117 | 118 | if _, err = part.Write(fileContent); err != nil { 119 | l.Debug("Error happened when write file content of task", taskID, "to form:", err) 120 | return nil, err 121 | } 122 | l.Debug("Write file content of task", taskID, "to form file successfully") 123 | 124 | shaOfFile := sha1.Sum(fileContent) 125 | shaString := strings.ToUpper(hex.EncodeToString(shaOfFile[:])) 126 | l.Info("Get sha256 of task", taskID, ":", shaString) 127 | 128 | params := map[string]string{ 129 | "op": "upload", 130 | "sha": shaString, 131 | "insertOnly": "0", 132 | } 133 | 134 | for key, val := range params { 135 | if err = writer.WriteField(key, val); err != nil { 136 | l.Error("Error happened when try to write params [", key, "=", val, "] to form in task", taskID, ":", err) 137 | return nil, err 138 | } 139 | l.Debug("Write params [", key, "=", val, "] to form in task", taskID, "successfully") 140 | } 141 | 142 | if err = writer.Close(); err != nil { 143 | l.Debug("Error happened when close form writer of task", taskID, ":", err) 144 | return nil, err 145 | } 146 | l.Debug("Close form writer of task", taskID, "successfully") 147 | 148 | rawUrl := fmt.Sprintf(uploadBaseURL, appID, bucketName, bucketPath, taskID) 149 | 150 | req, err := http.NewRequest("POST", rawUrl, body) 151 | if err != nil { 152 | l.Debug("Error happened when create post request of task", taskID, ":", err) 153 | return nil, err 154 | } 155 | l.Debug("Create request of task", taskID, "successfully") 156 | 157 | req.Header.Set("Content-Type", writer.FormDataContentType()) 158 | 159 | if time.Now().UTC().Add(time.Hour).After(c.expire) { 160 | newSign, newExpire := makeSign(nil, nil, nil) 161 | c.sign = newSign 162 | c.expire = newExpire 163 | l.Info("Renew sign, next expire date:", newExpire) 164 | } 165 | 166 | req.Header.Set("Authorization", c.sign) 167 | 168 | return req, nil 169 | } 170 | 171 | func (c *cosClient) Upload(q *plugins.SaveRequest, taskID string) error { 172 | req, err := c.auxMakeUploadRequest(q, taskID) 173 | if err != nil { 174 | l.Error("Error happened when create upload request of task", taskID, ":", err) 175 | return err 176 | } 177 | l.Debug("Create upload request of task", taskID, "successfully") 178 | 179 | res, err := c.Do(req) 180 | if err != nil { 181 | l.Error("Error happened when send request or get response of task", taskID, ":", err) 182 | return err 183 | } 184 | l.Debug("Send request and get response of task", taskID, "successfully") 185 | 186 | resContent, err := ioutil.ReadAll(res.Body) 187 | 188 | //noinspection GoUnhandledErrorResult 189 | defer res.Body.Close() 190 | 191 | if err != nil { 192 | l.Error("Error when read response body of task", taskID, ":", err) 193 | return err 194 | } 195 | l.Debug("Read response body of task", taskID, "successfully") 196 | 197 | var resJSON interface{} 198 | err = json.Unmarshal(resContent, &resJSON) 199 | if err != nil { 200 | l.Error("Error happened when parer response body as json:", err) 201 | return err 202 | } 203 | 204 | m := resJSON.(map[string]interface{}) 205 | jsonString := fmt.Sprintf("%#v", m) 206 | 207 | l.Info("Get response json:", jsonString) 208 | 209 | code := m["code"].(float64) 210 | if code != 0 { 211 | errorMsg := m["message"].(string) 212 | l.Error("Error happened when upload", taskID, ":", errorMsg) 213 | return errors.New(errorMsg) 214 | } 215 | l.Debug("Image upload of task", taskID, "successfully") 216 | 217 | if bucketHost == "" { 218 | data := m["data"].(map[string]interface{}) 219 | accessUrl := data["access_url"].(string) 220 | bucketHost = strings.Replace(accessUrl, taskID, taskIDPlaceholder, -1) 221 | l.Debug("Get image accessUrl format:", bucketHost) 222 | } 223 | 224 | return nil 225 | } 226 | 227 | func buildPath(taskID string) string { 228 | if len(bucketPath) > 0 { 229 | return bucketPath + "/" + taskID 230 | } 231 | return taskID 232 | } 233 | 234 | func (c *cosSdkV5Client) Upload(q *plugins.SaveRequest, taskID string) error { 235 | _, e := c.Client.Object.Put(context.Background(), buildPath(taskID), q.File, nil) 236 | bucketHost = strings.Replace(fmt.Sprintf(baseCosV5Url+"/%s%s", bucketName, appID, region, bucketPath, taskID), taskID, taskIDPlaceholder, -1) 237 | return e 238 | } 239 | -------------------------------------------------------------------------------- /plugins/tencent/cos/cos.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import "github.com/7sDream/rikka/plugins" 4 | 5 | type tencentCloudObjectStoragePlugin struct{} 6 | 7 | var ( 8 | l = plugins.SubLogger("[TC-COS]") 9 | 10 | appID string 11 | secretID string 12 | secretKey string 13 | bucketName string 14 | bucketPath string 15 | bucketHost string 16 | region string 17 | version string 18 | 19 | client genericCosClient 20 | 21 | // Plugin is the main plugin instance 22 | Plugin tencentCloudObjectStoragePlugin 23 | ) 24 | 25 | func (plugin tencentCloudObjectStoragePlugin) ExtraHandlers() []plugins.HandlerWithPattern { 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /plugins/tencent/cos/cosargs.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/7sDream/rikka/common/logger" 7 | ) 8 | 9 | // ArgCosVersion is flags for set Tencent cloud cos sdk version 10 | var ArgCosVersion = flag.String("tccosVer", "v4", "Tencent cos sdk version, v4(default) or v5") 11 | 12 | // GetVersionWitchCheck get the version of Tencent cos version from arguments 13 | func GetVersionWitchCheck(l *logger.Logger) string { 14 | if l == nil { 15 | l = logger.NewLogger("[CosArgs]") 16 | } 17 | value := *ArgCosVersion 18 | if value != "v5" && value != "v4" { 19 | l.Fatal("Tencent cos sdk version provided as v5 or v4") 20 | } 21 | return value 22 | } 23 | -------------------------------------------------------------------------------- /plugins/tencent/cos/init.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "github.com/7sDream/rikka/plugins" 5 | "github.com/7sDream/rikka/plugins/tencent" 6 | ) 7 | 8 | func (plugin tencentCloudObjectStoragePlugin) Init() { 9 | l.Info("Start plugin tencent cloud object storage") 10 | 11 | plugins.CheckCommonArgs(true, false) 12 | 13 | appID = tencent.GetAppIDWithCheck(l) 14 | secretID = tencent.GetSecretIDWithCheck(l) 15 | secretKey = tencent.GetSecretKeyWithCheck(l) 16 | bucketName = plugins.GetBucketName() 17 | bucketPath = plugins.GetBucketPath() 18 | version = GetVersionWitchCheck(l) 19 | 20 | if "v5" == version { 21 | region = tencent.GetRegionWithCheck(l) 22 | client = newCosSdkV5Client() 23 | } else if "v4" == version { 24 | client = newCosClient() 25 | } 26 | 27 | l.Info("Tencent cloud object storage plugin start successfully") 28 | } 29 | -------------------------------------------------------------------------------- /plugins/tencent/cos/save.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "github.com/7sDream/rikka/api" 5 | "github.com/7sDream/rikka/plugins" 6 | "github.com/satori/go.uuid" 7 | ) 8 | 9 | func uploadToCos(q *plugins.SaveRequest, taskID string) { 10 | defer func() { 11 | if err := recover(); err != nil { 12 | l.Error("Panic happened in background:", err) 13 | var errorMsg string 14 | switch t := err.(type) { 15 | case string: 16 | errorMsg = t 17 | case error: 18 | errorMsg = t.Error() 19 | default: 20 | errorMsg = "Unknown" 21 | } 22 | if err = plugins.ChangeTaskState(api.BuildErrorState(taskID, errorMsg)); err != nil { 23 | l.Fatal("Unable to change task", taskID, "to error state:", err) 24 | } 25 | } 26 | }() 27 | 28 | err := plugins.ChangeTaskState(buildUploadingState(taskID)) 29 | if err != nil { 30 | l.Fatal("Error happened when change state of task", taskID, "to uploading:", err) 31 | } 32 | l.Debug("Change state of task", taskID, "to uploading successfully") 33 | 34 | l.Debug("Uploading", taskID, "to your cos bucket...") 35 | 36 | if err := client.Upload(q, taskID); err != nil { 37 | l.Error("Error happened when upload image", taskID, "to cos:", err) 38 | if err = plugins.ChangeTaskState(api.BuildErrorState(taskID, err.Error())); err != nil { 39 | l.Fatal("Unable to change task", taskID, "to error state:", err) 40 | } 41 | return 42 | } 43 | l.Debug("Upload image", taskID, "to cos successfully") 44 | 45 | if err := plugins.DeleteTask(taskID); err != nil { 46 | l.Error("Error happened when delete task", taskID, ":", err) 47 | } 48 | l.Debug("Delete task", taskID, "successfully") 49 | } 50 | 51 | func (plugin tencentCloudObjectStoragePlugin) SaveRequestHandle(q *plugins.SaveRequest) (*api.TaskId, error) { 52 | l.Debug("Receive a file save request") 53 | taskID := uuid.NewV4().String() + "." + q.FileExt 54 | 55 | err := plugins.CreateTask(taskID) 56 | if err != nil { 57 | l.Fatal("Error happened when create new task!") 58 | } 59 | l.Debug("create task", taskID, "successfully, starting background task") 60 | 61 | go uploadToCos(q, taskID) 62 | 63 | l.Debug("Background task started, return task ID:", taskID) 64 | return &api.TaskId{TaskId: taskID}, nil 65 | } 66 | -------------------------------------------------------------------------------- /plugins/tencent/cos/state.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "github.com/7sDream/rikka/api" 5 | "github.com/7sDream/rikka/plugins" 6 | ) 7 | 8 | const ( 9 | stateUploading = "uploading" 10 | stateUploadingCode = 2 11 | stateUploadingDesc = "Rikka is uploading your image to Tencent COS" 12 | ) 13 | 14 | func buildUploadingState(taskID string) *api.State { 15 | return &api.State{ 16 | TaskID: taskID, 17 | State: stateUploading, 18 | StateCode: stateUploadingCode, 19 | Description: stateUploadingDesc, 20 | } 21 | } 22 | 23 | func (plugin tencentCloudObjectStoragePlugin) StateRequestHandle(taskID string) (*api.State, error) { 24 | pState, err := plugins.GetTaskState(taskID) 25 | if err != nil { 26 | // Not exist as finished 27 | return api.BuildFinishState(taskID), nil 28 | } 29 | return pState, nil 30 | } 31 | -------------------------------------------------------------------------------- /plugins/tencent/cos/url.go: -------------------------------------------------------------------------------- 1 | package cos 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/7sDream/rikka/api" 8 | "github.com/7sDream/rikka/plugins" 9 | ) 10 | 11 | func (plugin tencentCloudObjectStoragePlugin) URLRequestHandle(q *plugins.URLRequest) (*api.URL, error) { 12 | if bucketHost == "" { 13 | l.Error("Request URL of task", q.TaskID, "before state become to finish") 14 | return nil, errors.New("get url before task finish") 15 | } 16 | 17 | if !strings.Contains(bucketHost, taskIDPlaceholder) { 18 | l.Fatal("Error! Unable to get image url from bucket host:", bucketHost) 19 | } 20 | 21 | return &api.URL{ 22 | URL: strings.Replace(bucketHost, taskIDPlaceholder, q.TaskID, -1), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /plugins/tencent/envvar.go: -------------------------------------------------------------------------------- 1 | package tencent 2 | 3 | import ( 4 | "github.com/7sDream/rikka/common/logger" 5 | "github.com/7sDream/rikka/common/util" 6 | ) 7 | 8 | const ( 9 | envAppIDKey = "RIKKA_TENCENT_APPID" 10 | envSecretIDKey = "RIKKA_TENCENT_SECRETID" 11 | envSecretKeyKey = "RIKKA_TENCENT_SECRETKEY" 12 | envRegionKey = "RIKKA_TENCENT_REGION" 13 | ) 14 | 15 | // GetAppIDWithCheck will get APPID of Tencent Cloud. 16 | // If it is empty, will raise a Fatal. 17 | func GetAppIDWithCheck(l *logger.Logger) string { 18 | return util.GetEnvWithCheck("AppID", envAppIDKey, l) 19 | } 20 | 21 | // GetSecretIDWithCheck will get SecretID of Tencent Cloud. 22 | // If it is empty, will raise a Fatal. 23 | func GetSecretIDWithCheck(l *logger.Logger) string { 24 | return util.GetEnvWithCheck("SecretID", envSecretIDKey, l) 25 | } 26 | 27 | // GetSecretKeyWithCheck will get SecretKey of Tencent Cloud. 28 | // If it is empty, will raise a Fatal. 29 | func GetSecretKeyWithCheck(l *logger.Logger) string { 30 | return util.GetEnvWithCheck("SecretKey", envSecretKeyKey, l) 31 | } 32 | 33 | // GetRegionWithCheck will get Region of Tencent Cloud. 34 | // If it is empty, will raise a Fatal 35 | func GetRegionWithCheck(l *logger.Logger) string { 36 | return util.GetEnvWithCheck("Region", envRegionKey, l) 37 | } 38 | -------------------------------------------------------------------------------- /plugins/types.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "mime/multipart" 5 | "net/http" 6 | 7 | "github.com/7sDream/rikka/api" 8 | ) 9 | 10 | // SaveRequest is a request that want to 'save'(actually upload) a file. 11 | // Plugins' SaveRequestHandle func should accept a point of instance and return a string as taskID 12 | type SaveRequest struct { 13 | File multipart.File 14 | FileSize int64 15 | FileExt string 16 | } 17 | 18 | // URLRequest is a request ask for photo src url of a task. 19 | // Plugins' URLRequestHandle func should accept a point of instance and return a string as URL 20 | type URLRequest struct { 21 | HTTPRequest *http.Request 22 | TaskID string 23 | PicOp *ImageOperate 24 | IsServeTLS bool // only fs plugin use this field 25 | } 26 | 27 | // ImageOperate stand for some operate of src image, not used now. 28 | type ImageOperate struct { 29 | Width int 30 | Height int 31 | Rotate int 32 | OtherArg string 33 | } 34 | 35 | // HandlerWithPattern is a struct combine a http.Handler with the pattern is will server. 36 | // Plugins' ExtraHandlers func return an array of this and will be added to http.handler when init plugin. 37 | type HandlerWithPattern struct { 38 | Pattern string 39 | Handler http.HandlerFunc 40 | } 41 | 42 | // RikkaPlugin is plugin interface, all plugin should implement those function. 43 | type RikkaPlugin interface { 44 | // Init will be called when load plugin. 45 | Init() 46 | // AcceptFile will call this. 47 | SaveRequestHandle(*SaveRequest) (*api.TaskId, error) 48 | // GetState will call this. 49 | StateRequestHandle(string) (*api.State, error) 50 | // GetURL will call this. 51 | URLRequestHandle(q *URLRequest) (*api.URL, error) 52 | // Will be added into http handler list. 53 | ExtraHandlers() []HandlerWithPattern 54 | } 55 | -------------------------------------------------------------------------------- /plugins/upai/README.md: -------------------------------------------------------------------------------- 1 | # Upai Plugin 2 | 3 | [中文版][version-zh] 4 | 5 | Added in version 0.3.0. Inner name `upai`. 6 | 7 | ## Description 8 | 9 | This plugin use UPai Cloud CND to store your image. 10 | 11 | ## Options 12 | 13 | You should provide UPai operator name, password, bucket name and bucket host. 14 | 15 | operator name and password should be add into your env variable, use key `RIKKA_UPAI_OPERATOR` and `RIKKA_UPAI_PASSWORD`. 16 | 17 | Bucket name and bucket host should be provide use command line option: 18 | 19 | `-bname` for the bucket name. 20 | 21 | `-bhost` for bucket host. 22 | 23 | BTW: you can set upload dir by provide `-bpath` option. 24 | 25 | For example,ues `-bpath rikka`, then images will be under `rikka` folder。 26 | 27 | Multi-level dir like `-bpath rikka/images` are also supported. 28 | 29 | ## Guide 30 | 31 | See [Rikka Deploy Guide with UPai plugin on DaoCloud][upai-plugin-guide] 32 | 33 | [version-zh]: https://github.com/7sDream/rikka/blob/master/plugins/upai/README.zh.md 34 | [upai-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E5%8F%88%E6%8B%8D%E4%BA%91%E6%8F%92%E4%BB%B6 35 | -------------------------------------------------------------------------------- /plugins/upai/README.zh.md: -------------------------------------------------------------------------------- 1 | # UPai 插件 2 | 3 | [English version][version-en] 4 | 5 | 0.3.0 版本添加。内部名 `upai`。 6 | 7 | ## 介绍 8 | 9 | 这个插件使用又拍云 CND 来储存你上传的图片。 10 | 11 | ## 参数 12 | 13 | 你需要提供四个参数:又拍云的操作员名和密码,以及图片要保存到的空间名和空间域名。 14 | 15 | 操作员名和密码使用环境变量的形式提供,变量名 `RIKKA_UPAI_OPERATOR` 和 `RIKKA_UPAI_PASSWORD`。 16 | 17 | 空间名和空间域名则通过命令行参数提供: 18 | 19 | `-bname` 空间名 20 | 21 | `-bhost` 空间域名 22 | 23 | 另外,你还可以通过提供 `bpath` 参数的形式设置图片需要保存到的文件夹。 24 | 25 | 比如,使用 `-bpath rikka`,上传到的文件会传到空间的 `rikka` 文件夹下。当然 `-bpath rikka/images` 之类的多级文件夹也是可以的。 26 | 27 | ## 部署教程 28 | 29 | 请看部署教程:[在 DaoCloud 上部署使用又拍云插件的 Rikka][upai-plugin-guide]。 30 | 31 | [version-en]: https://github.com/7sDream/rikka/blob/master/plugins/upai/README.md 32 | [upai-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E5%8F%88%E6%8B%8D%E4%BA%91%E6%8F%92%E4%BB%B6 33 | -------------------------------------------------------------------------------- /plugins/upai/init.go: -------------------------------------------------------------------------------- 1 | package upai 2 | 3 | import ( 4 | "github.com/7sDream/rikka/common/util" 5 | "github.com/7sDream/rikka/plugins" 6 | "github.com/upyun/go-sdk/v3/upyun" 7 | ) 8 | 9 | func (qnp upaiPlugin) Init() { 10 | l.Info("Start plugin upai") 11 | 12 | plugins.CheckCommonArgs(true, true) 13 | 14 | operator = util.GetEnvWithCheck("Operator", operatorEnvKey, l) 15 | password = util.GetEnvWithCheck("Password", passwordEnvKey, l) 16 | bucketName = plugins.GetBucketName() 17 | bucketAddr = plugins.GetBucketHost() 18 | bucketPrefix = plugins.GetBucketPath() 19 | 20 | client = upyun.NewUpYun(&upyun.UpYunConfig{ 21 | Bucket: bucketName, 22 | Operator: operator, 23 | Password: password, 24 | }) 25 | 26 | l.Info("UPai plugin start successfully") 27 | } 28 | -------------------------------------------------------------------------------- /plugins/upai/save.go: -------------------------------------------------------------------------------- 1 | package upai 2 | 3 | import ( 4 | "github.com/7sDream/rikka/api" 5 | "github.com/7sDream/rikka/plugins" 6 | "github.com/satori/go.uuid" 7 | "github.com/upyun/go-sdk/v3/upyun" 8 | ) 9 | 10 | func buildPath(taskID string) string { 11 | return bucketPrefix + taskID 12 | } 13 | 14 | func uploadToUPai(taskID string, q *plugins.SaveRequest) { 15 | // preparing... 16 | err := plugins.ChangeTaskState(buildUploadingState(taskID)) 17 | if err != nil { 18 | l.Fatal("Error happen when change state of task", taskID, "to uploading:", err) 19 | } 20 | l.Debug("Change state of task", taskID, "to uploading successfully") 21 | 22 | l.Debug("Uploading to UPai cloud...") 23 | 24 | err = client.Put(&upyun.PutObjectConfig{ 25 | Path: buildPath(taskID), 26 | Reader: q.File, 27 | UseResumeUpload: false, 28 | }) 29 | 30 | if err != nil { 31 | l.Error("Error happened when upload to upai:", err) 32 | err = plugins.ChangeTaskState(api.BuildErrorState(taskID, err.Error())) 33 | if err != nil { 34 | l.Fatal("Error happened when change state of task", taskID, "to error:", err) 35 | } 36 | l.Debug("Change state of task", taskID, "to error successfully") 37 | return 38 | } 39 | // uploading successfully 40 | l.Info("Upload task", taskID, "to upai cloud successfully") 41 | 42 | err = plugins.DeleteTask(taskID) 43 | if err != nil { 44 | l.Fatal("Error happened when delete state of task", taskID, ":", err) 45 | } 46 | l.Debug("Delete task", taskID, "successfully") 47 | } 48 | 49 | func (qnp upaiPlugin) SaveRequestHandle(q *plugins.SaveRequest) (*api.TaskId, error) { 50 | l.Debug("Receive a file save request") 51 | 52 | taskID := uuid.NewV4().String() + "." + q.FileExt 53 | 54 | err := plugins.CreateTask(taskID) 55 | if err != nil { 56 | l.Fatal("Error happened when create new task!") 57 | } 58 | l.Debug("create task", taskID, "successfully, starting background task") 59 | 60 | go uploadToUPai(taskID, q) 61 | 62 | l.Debug("Background task started, return task ID:", taskID) 63 | return &api.TaskId{TaskId: taskID}, nil 64 | } 65 | -------------------------------------------------------------------------------- /plugins/upai/state.go: -------------------------------------------------------------------------------- 1 | package upai 2 | 3 | import ( 4 | "github.com/7sDream/rikka/api" 5 | "github.com/7sDream/rikka/plugins" 6 | ) 7 | 8 | const ( 9 | stateUploading = "uploading" 10 | stateUploadingCode = 2 11 | stateUploadingDesc = "Rikka is uploading your image to UPai cloud" 12 | ) 13 | 14 | func buildUploadingState(taskID string) *api.State { 15 | return &api.State{ 16 | TaskID: taskID, 17 | State: stateUploading, 18 | StateCode: stateUploadingCode, 19 | Description: stateUploadingDesc, 20 | } 21 | } 22 | 23 | func (qnp upaiPlugin) StateRequestHandle(taskID string) (pState *api.State, err error) { 24 | l.Debug("Receive a state request of taskID", taskID) 25 | 26 | pState, err = plugins.GetTaskState(taskID) 27 | if err == nil { 28 | if pState.StateCode == api.StateErrorCode { 29 | l.Warn("Get a error state of task", taskID, *pState) 30 | } else { 31 | l.Debug("Get a normal state of task", taskID, *pState) 32 | } 33 | return pState, nil 34 | } 35 | 36 | l.Debug("State of task", taskID, "not found, just return a finish state") 37 | return api.BuildFinishState(taskID), nil 38 | } 39 | -------------------------------------------------------------------------------- /plugins/upai/upai.go: -------------------------------------------------------------------------------- 1 | package upai 2 | 3 | import ( 4 | "github.com/7sDream/rikka/plugins" 5 | "github.com/upyun/go-sdk/v3/upyun" 6 | ) 7 | 8 | type upaiPlugin struct{} 9 | 10 | const ( 11 | operatorEnvKey = "RIKKA_UPAI_OPERATOR" 12 | passwordEnvKey = "RIKKA_UPAI_PASSWORD" 13 | ) 14 | 15 | var ( 16 | l = plugins.SubLogger("[UPai]") 17 | 18 | operator string 19 | password string 20 | bucketName string 21 | bucketAddr string 22 | bucketPrefix string 23 | 24 | // Plugin is the main plugin instance 25 | Plugin = upaiPlugin{} 26 | 27 | client *upyun.UpYun 28 | ) 29 | 30 | func (qnp upaiPlugin) ExtraHandlers() []plugins.HandlerWithPattern { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /plugins/upai/url.go: -------------------------------------------------------------------------------- 1 | package upai 2 | 3 | import ( 4 | "github.com/7sDream/rikka/api" 5 | "github.com/7sDream/rikka/plugins" 6 | ) 7 | 8 | func buildURL(taskID string) string { 9 | return bucketAddr + "/" + buildPath(taskID) 10 | } 11 | 12 | // URLRequestHandle will be called when receive a get image url by taskID request 13 | func (qnp upaiPlugin) URLRequestHandle(q *plugins.URLRequest) (pURL *api.URL, err error) { 14 | l.Debug("Receive an url request of task", q.TaskID) 15 | return &api.URL{URL: buildURL(q.TaskID)}, nil 16 | } 17 | -------------------------------------------------------------------------------- /plugins/weibo/README.md: -------------------------------------------------------------------------------- 1 | # Sina Weibo Plugin 2 | 3 | [中文版][version-zh] 4 | 5 | Added in version 0.3.1. Inner name `weibo`. 6 | 7 | ## Description 8 | 9 | This plugin use Sina weibo to store your image. 10 | 11 | ## Options 12 | 13 | You should provide a cookies string which stand for a logged weibo account in env var `RIKKA_WEIBO_COOKIES`. 14 | 15 | And you should provide a password (with option `-ucpwd`, update cookies password) which will be checked when you update cookies from `/cookies` page. Default password is `weibo`. 16 | 17 | Format of cookies string: 18 | 19 | ```text 20 | FOO=foofoofoof; BAR=barbarbarb; ZOO=zoozozozozozo 21 | ``` 22 | 23 | Notice: You should provide **ALL** cookies of weibo.com, contains those be tag with **HTTPOnly**. 24 | 25 | ## A way of get cookies string 26 | 27 | 1. Launch **Chrome** 28 | 2. visit http://weibo.com 29 | 3. Login if you haven't 30 | 4. `F12` to open dev tools, turn to `Network` tab 31 | 5. Refresh page 32 | 6. Click first request(starts with `home`) in the left list 33 | 7. Find `Cookies` field of `Request Header` in the request content(right side), copy field value(without the `Cookies: ` prefix) 34 | 35 | Tutorial with image can be find in [Guide](#Guide) section. 36 | 37 | ## Update cookies after launch 38 | 39 | After you deploy and launch Rikka, you can update weibo cookies when expired. 40 | 41 | Visit `/cookies` page, put new cookies string into first textarea, your `ucpwd` into second, and submit. 42 | 43 | If update successfully, you will be redirect to homepage of Rikka. And Error message will shown if error happened. 44 | 45 | ## Guide 46 | 47 | See [Rikka Deploy Guide with Weibo plugin on DaoCloud][weibo-plugin-guide] 48 | 49 | [version-zh]: https://github.com/7sDream/rikka/blob/master/plugins/weibo/README.zh.md 50 | [weibo-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E6%96%B0%E6%B5%AA%E5%BE%AE%E5%8D%9A%E6%8F%92%E4%BB%B6 51 | -------------------------------------------------------------------------------- /plugins/weibo/README.zh.md: -------------------------------------------------------------------------------- 1 | # Weibo 新浪微博插件 2 | 3 | [English version][version-en] 4 | 5 | 0.3.1 版本添加。内部名 `weibo`. 6 | 7 | ## 简介 8 | 9 | 这个插件使用新浪微博发送微博时的上传图片接口作为图片的最终储存方式。 10 | 11 | ## 参数 12 | 13 | 你需要提供一个已登录的微博用户的 Cookies 字符串,储存在环境变量 `RIKKA_WEIBO_COOKIES` 里。 14 | 15 | 你还需要通过 `-ucpwd` 参数提供一个用于在 Web 界面(`/cookies`)上更新 Cookies 时需要输入的密码。如果你不提供的话,默认密码是 `weibo`。(`ucpwd` 的意思是 Update Cookies PassWorD) 16 | 17 | Cookie 字符串的格式大概是: 18 | 19 | ```text 20 | FOO=foofoofoof; BAR=barbarbarb; ZOO=zoozozozozozo 21 | ``` 22 | 23 | 注意:你需要提供 weibo.com 下的**所有** Cookies,包括含有 `HTTPOnly` 属性的。 24 | 25 | ## 获取完整 Cookies 字符串 26 | 27 | 1. 启动 **Chrome** 浏览器 28 | 2. 访问 http://weibo.com 29 | 3. 登录微博(如果现在没登录的话) 30 | 4. 按 `F12` 打开开发人员工具, 转到 `Network` 31 | 5. 刷新页面 32 | 6. 找到请求列表里的第一个请求(以 `home` 开头),点击它 33 | 7. 在右边的请求内容里找到 `Request Header` 里的 `Cookies` 字段,复制字段值。(不包括前面的 `Cookies: `) 34 | 35 | 图文教程请看[部署教程](#部署教程)一节。 36 | 37 | ## 启动后更新 Cookies 38 | 39 | 在你部署并启动 Rikka 后,你可以在 Cookies 过期后通过一个 Web 页面更新它。 40 | 41 | 访问 `/cookies` 页面,把新的 Cookies 字符串复制进第一个文本框里,第二个框里填 Cookies 更新密码(就是在启动 Rikka 时提供的 `-ucpwd` 参数),点击提交。 42 | 43 | 如果更新成功,你会被转到 Rikka 的首页。如果失败了则会显示错误信息。 44 | 45 | ## 部署教程 46 | 47 | 请看部署教程:[在 DaoCloud 上部署使用新浪微博插件的 Rikka][weibo-plugin-guide]。 48 | 49 | [version-en]: https://github.com/7sDream/rikka/blob/master/plugins/weibo/README.md 50 | [weibo-plugin-guide]: https://github.com/7sDream/rikka/wiki/%E4%BD%BF%E7%94%A8%E6%96%B0%E6%B5%AA%E5%BE%AE%E5%8D%9A%E6%8F%92%E4%BB%B6 51 | -------------------------------------------------------------------------------- /plugins/weibo/client.go: -------------------------------------------------------------------------------- 1 | package weibo 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | "net/http/cookiejar" 13 | "net/textproto" 14 | "net/url" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "github.com/7sDream/rikka/plugins" 20 | ) 21 | 22 | var ( 23 | weiboURL, _ = url.Parse("http://weibo.com") 24 | 25 | fileFieldKey = "pic1" 26 | imageIDKey = "pid" 27 | imageURLPrefix = "http://ww1.sinaimg.cn/large/" 28 | cbBase = "http://weibo.com/aj/static/upimgback.html?_wv=5&callback=STK_ijax_" 29 | uploadURLBase = "http://picupload.service.weibo.com/interface/pic_upload.php" 30 | ) 31 | 32 | const ( 33 | miniPublishPageURL = "http://weibo.com/minipublish" 34 | ) 35 | 36 | func buildURL(pid string) string { 37 | return imageURLPrefix + pid 38 | } 39 | 40 | func newWeiboClient() *http.Client { 41 | l.Debug("Creating weibo client") 42 | 43 | cookieJar, err := cookiejar.New(nil) 44 | if err != nil { 45 | l.Fatal("Error happened when create cookie jar:", err) 46 | } 47 | l.Debug("Create cookie jar successfully") 48 | 49 | l.Debug("Create weibo client successfully") 50 | return &http.Client{ 51 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 52 | return http.ErrUseLastResponse 53 | }, 54 | Jar: cookieJar, 55 | } 56 | } 57 | 58 | func updateCookies(cookieStr string) error { 59 | l.Debug("Updating cookies") 60 | rawRequest := fmt.Sprintf("GET / HTTP/1.0\r\nCookie: %s\r\n\r\n", cookieStr) 61 | 62 | req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(rawRequest))) 63 | if err != nil { 64 | l.Error("Error when parse cookies from string", cookieStr, ":", err) 65 | return err 66 | } 67 | l.Debug("Parse cookie from string successfully") 68 | 69 | cookies := req.Cookies() 70 | 71 | if len(cookies) == 0 { 72 | errorMsg := "no cookies data in string your provided" 73 | l.Error(errorMsg) 74 | return errors.New(errorMsg) 75 | } 76 | for _, cookie := range cookies { 77 | l.Debug(fmt.Sprintf("%#v", cookie)) 78 | if cookie.Value == "" { 79 | errorMsg := "A non-value cookie key: " + cookie.Name 80 | l.Error(errorMsg) 81 | return errors.New(errorMsg) 82 | } 83 | } 84 | l.Debug("Check cookies passed") 85 | 86 | client.Jar, err = cookiejar.New(nil) 87 | if err != nil { 88 | l.Error("Error happened when create new cookie jar:", err) 89 | return err 90 | } 91 | l.Debug("Create new cookie jar successfully") 92 | 93 | client.Jar.SetCookies(weiboURL, cookies) 94 | return nil 95 | } 96 | 97 | func auxCheckLogin() (bool, error) { 98 | l.Debug("Checking is login...") 99 | res, err := client.Get(miniPublishPageURL) 100 | if err != nil { 101 | l.Error("Error happened when visit mini publish page:", err) 102 | return false, err 103 | } 104 | l.Debug("Visit mini publish page successfully, code", res.StatusCode) 105 | 106 | //noinspection GoUnhandledErrorResult 107 | defer res.Body.Close() 108 | 109 | return res.StatusCode == http.StatusOK, nil 110 | } 111 | 112 | func auxCalcCB() string { 113 | return cbBase + strconv.FormatInt(time.Now().Unix(), 10) 114 | } 115 | 116 | func auxGetUploadURL() string { 117 | 118 | //noinspection SpellCheckingInspection 119 | uploadQuery := map[string]string{ 120 | "cb": auxCalcCB(), 121 | "url": "", 122 | "markpos": "1", 123 | "logo": "0", 124 | "nick": "", 125 | "mask": "0", 126 | "app": "miniblog", 127 | "s": "rdxt", 128 | } 129 | 130 | uploadURL, _ := url.Parse(uploadURLBase) 131 | query := uploadURL.Query() 132 | for key, val := range uploadQuery { 133 | query.Set(key, val) 134 | } 135 | uploadURL.RawQuery = query.Encode() 136 | 137 | return uploadURL.String() 138 | } 139 | 140 | var quoteEscape = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 141 | 142 | func escapeQuotes(s string) string { 143 | return quoteEscape.Replace(s) 144 | } 145 | 146 | func auxCreateImageFormFileField(w *multipart.Writer, fileFieldKey string, filename string, fileType string) (io.Writer, error) { 147 | h := make(textproto.MIMEHeader) 148 | h.Set("Content-Disposition", 149 | fmt.Sprintf(`form-data; name="%s"; filename="%s"`, 150 | escapeQuotes(fileFieldKey), escapeQuotes(filename))) 151 | h.Set("Content-Type", "image/"+fileType) 152 | return w.CreatePart(h) 153 | } 154 | 155 | func auxCreateUploadRequest(q *plugins.SaveRequest) (*http.Request, error) { 156 | l.Debug("Creating upload request...") 157 | 158 | body := &bytes.Buffer{} 159 | writer := multipart.NewWriter(body) 160 | 161 | part, err := auxCreateImageFormFileField(writer, fileFieldKey, "noname."+q.FileExt, q.FileExt) 162 | if err != nil { 163 | l.Error("Error happened when create form file:", err) 164 | return nil, err 165 | } 166 | l.Debug("Create form writer successfully") 167 | 168 | content, err := ioutil.ReadAll(q.File) 169 | if err != nil { 170 | l.Error("Error happened when read file content") 171 | return nil, err 172 | } 173 | l.Debug("Read file content successfully") 174 | 175 | if _, err = part.Write(content); err != nil { 176 | l.Error("Error happened when write file content to form:", err) 177 | return nil, err 178 | } 179 | l.Debug("Write file content to form file successfully") 180 | 181 | if err = writer.Close(); err != nil { 182 | l.Error("Error happened when close form writer:", err) 183 | return nil, err 184 | } 185 | l.Debug("Close form writer successfully") 186 | 187 | uploadURL := auxGetUploadURL() 188 | 189 | req, err := http.NewRequest("POST", uploadURL, body) 190 | if err != nil { 191 | l.Error("Error happened when create post request with url", uploadURL, ":", err) 192 | return nil, err 193 | } 194 | 195 | var cookies []string 196 | for _, cookie := range client.Jar.Cookies(weiboURL) { 197 | cookies = append(cookies, cookie.Name+"="+cookie.Value) 198 | } 199 | 200 | req.Header.Set("Cookie", strings.Join(cookies, "; ")) 201 | req.Header.Set("Content-Type", writer.FormDataContentType()) 202 | 203 | l.Debug("Create post request successfully, url", uploadURL) 204 | 205 | return req, nil 206 | } 207 | 208 | func auxGetImageID(raw string) (string, error) { 209 | l.Debug("Getting image url from redirect url", raw) 210 | 211 | redirectURL, err := url.Parse(raw) 212 | if err != nil { 213 | l.Error("Error happened when parse redirect URL ", redirectURL, ":", err) 214 | return "", err 215 | } 216 | l.Debug("parse redirect URL", raw, "successfully") 217 | 218 | imageID := redirectURL.Query().Get(imageIDKey) 219 | if imageID == "" { 220 | errorMsg := "No image ID field " + imageIDKey + " in url " + raw + ", weibo api changed" 221 | l.Error("Error happened when get image id:", errorMsg) 222 | return "", errors.New(errorMsg) 223 | } 224 | l.Debug("Get image ID", imageID, "from url", raw, "successfully") 225 | 226 | return imageID, nil 227 | } 228 | 229 | func auxUpload(q *plugins.SaveRequest) (string, error) { 230 | l.Debug("Truly uploading image...") 231 | 232 | req, err := auxCreateUploadRequest(q) 233 | if err != nil { 234 | l.Error("Error happened when create upload request:", err) 235 | return "", err 236 | } 237 | l.Debug("Create upload request successfully") 238 | 239 | res, err := client.Do(req) 240 | if err != nil { 241 | l.Error("Error happened when send upload request:", err) 242 | return "", err 243 | } 244 | 245 | if res.StatusCode != http.StatusFound { 246 | errorMsg := "upload response code is not 302, weibo api changed" 247 | l.Error("Error happened when get image url:", errorMsg) 248 | return "", errors.New(errorMsg) 249 | } 250 | 251 | redirectURL := res.Header.Get("Location") 252 | if redirectURL == "" { 253 | errorMsg := "no location header, weibo api changed" 254 | l.Error("Error happened when get image url:", errorMsg) 255 | return "", errors.New(errorMsg) 256 | } 257 | 258 | imageID, err := auxGetImageID(redirectURL) 259 | if err != nil { 260 | errorMsg := "can't get image url from Location header, weibo api changed" 261 | l.Error("Error happened when get image:", errorMsg) 262 | return "", errors.New(errorMsg) 263 | } 264 | 265 | return imageID, nil 266 | } 267 | 268 | func upload(q *plugins.SaveRequest) (string, error) { 269 | l.Debug("Preparing upload...") 270 | 271 | login, err := auxCheckLogin() 272 | if err != nil { 273 | l.Error("Error happened when check login:", err) 274 | return "", err 275 | } 276 | l.Debug("Check login successfully") 277 | 278 | if !login { 279 | l.Error("No weibo account login") 280 | return "", errors.New("weibo account not login, please update cookies") 281 | } 282 | l.Debug("Weibo account is logged") 283 | 284 | imageID, err := auxUpload(q) 285 | if err != nil { 286 | l.Error("Error happened when get image url:", err) 287 | return "", err 288 | } 289 | l.Debug("Get image url", imageID, "successfully") 290 | 291 | return imageID, nil 292 | } 293 | -------------------------------------------------------------------------------- /plugins/weibo/extra.go: -------------------------------------------------------------------------------- 1 | package weibo 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/7sDream/rikka/common/util" 8 | "github.com/7sDream/rikka/plugins" 9 | ) 10 | 11 | const ( 12 | updateCookiesFormHTML = ` 13 | 14 | 15 | 16 | Update cookies 17 | 18 | 19 |
20 |
21 |
22 | 23 |
24 | 25 | 26 | ` 27 | ) 28 | 29 | func (wbp weiboPlugin) ExtraHandlers() []plugins.HandlerWithPattern { 30 | updateCookiesFormHandler := plugins.HandlerWithPattern{ 31 | Pattern: "/cookies", 32 | Handler: util.RequestFilter( 33 | "/cookies", "GET", l, 34 | util.TemplateStringRenderHandler( 35 | "cookiesForm.html", updateCookiesFormHTML, nil, l, 36 | ), 37 | ), 38 | } 39 | 40 | updateCookiesHandler := plugins.HandlerWithPattern{ 41 | Pattern: "/update", 42 | Handler: util.RequestFilter( 43 | "/update", "POST", l, 44 | func(w http.ResponseWriter, r *http.Request) { 45 | if r.FormValue("password") != *argUpdateCookiesPassword { 46 | util.ErrHandle(w, errors.New("error password")) 47 | return 48 | } 49 | err := updateCookies(r.FormValue("cookies")) 50 | if util.ErrHandle(w, err) { 51 | return 52 | } 53 | http.Redirect(w, r, "/", http.StatusFound) 54 | }, 55 | ), 56 | } 57 | 58 | return []plugins.HandlerWithPattern{updateCookiesFormHandler, updateCookiesHandler} 59 | } 60 | -------------------------------------------------------------------------------- /plugins/weibo/init.go: -------------------------------------------------------------------------------- 1 | package weibo 2 | 3 | import "github.com/7sDream/rikka/common/util" 4 | 5 | func (wbp weiboPlugin) Init() { 6 | l.Info("Start plugin weibo") 7 | 8 | cookiesStr := util.GetEnvWithCheck("Cookies", cookiesEnvKey, l) 9 | 10 | client = newWeiboClient() 11 | 12 | if err := updateCookies(cookiesStr); err != nil { 13 | l.Fatal("Error happened when create cookies:", err) 14 | } 15 | 16 | l.Info("Arg update cookies password =", *argUpdateCookiesPassword) 17 | 18 | l.Info("Weibo plugin start successfully") 19 | } 20 | -------------------------------------------------------------------------------- /plugins/weibo/save.go: -------------------------------------------------------------------------------- 1 | package weibo 2 | 3 | import ( 4 | "strconv" 5 | "sync/atomic" 6 | 7 | "github.com/7sDream/rikka/api" 8 | "github.com/7sDream/rikka/plugins" 9 | ) 10 | 11 | func uploadToWeibo(taskIDInt int64, taskIDStr string, q *plugins.SaveRequest) { 12 | defer func() { 13 | if err := recover(); err != nil { 14 | l.Error("Panic happened in background:", err) 15 | var errorMsg string 16 | switch t := err.(type) { 17 | case string: 18 | errorMsg = t 19 | case error: 20 | errorMsg = t.Error() 21 | default: 22 | errorMsg = "Unknown" 23 | } 24 | _ = plugins.ChangeTaskState(api.BuildErrorState(taskIDStr, errorMsg)) 25 | } 26 | }() 27 | 28 | err := plugins.ChangeTaskState(buildUploadingState(taskIDStr)) 29 | if err != nil { 30 | l.Fatal("Error happened when change state of task", taskIDStr, "to uploading:", err) 31 | } 32 | l.Debug("Change state of task", taskIDStr, "to uploading successfully") 33 | 34 | l.Debug("Uploading to weibo...") 35 | url, err := upload(q) 36 | if err != nil { 37 | l.Error("Error happened when upload image to weibo:", err) 38 | err = plugins.ChangeTaskState(api.BuildErrorState(taskIDStr, err.Error())) 39 | if err != nil { 40 | l.Fatal("Error happened when change task", taskIDStr, "to error state:", err) 41 | } 42 | l.Debug("Change state of task", taskIDStr, "to error successfully") 43 | return 44 | } 45 | 46 | imageIDMap[taskIDInt] = url 47 | l.Info("Upload task", taskIDStr, "to weibo cloud successfully") 48 | 49 | err = plugins.DeleteTask(taskIDStr) 50 | if err != nil { 51 | delete(imageIDMap, taskIDInt) 52 | l.Fatal("Error happened when change task", taskIDStr, "to finish state:", err) 53 | } 54 | l.Debug("Delete task", taskIDStr, "successfully") 55 | } 56 | 57 | func (wbp weiboPlugin) SaveRequestHandle(q *plugins.SaveRequest) (*api.TaskId, error) { 58 | l.Debug("Receive a file save request") 59 | 60 | taskIDInt := atomic.AddInt64(&counter, 1) 61 | taskIDStr := strconv.FormatInt(taskIDInt, 10) 62 | 63 | err := plugins.CreateTask(taskIDStr) 64 | if err != nil { 65 | l.Fatal("Error happened when create new task!") 66 | } 67 | l.Debug("create task", taskIDStr, "successfully, starting background task") 68 | 69 | go uploadToWeibo(taskIDInt, taskIDStr, q) 70 | 71 | l.Debug("Background task started, return task ID:", taskIDStr) 72 | return &api.TaskId{TaskId: taskIDStr}, nil 73 | } 74 | -------------------------------------------------------------------------------- /plugins/weibo/state.go: -------------------------------------------------------------------------------- 1 | package weibo 2 | 3 | import ( 4 | "strconv" 5 | "sync/atomic" 6 | 7 | "github.com/7sDream/rikka/api" 8 | "github.com/7sDream/rikka/plugins" 9 | ) 10 | 11 | const ( 12 | stateUploading = "uploading" 13 | stateUploadingCode = 2 14 | stateUploadingDesc = "Rikka is uploading your image to weibo" 15 | ) 16 | 17 | func buildUploadingState(taskID string) *api.State { 18 | return &api.State{ 19 | TaskID: taskID, 20 | State: stateUploading, 21 | StateCode: stateUploadingCode, 22 | Description: stateUploadingDesc, 23 | } 24 | } 25 | 26 | func (wbp weiboPlugin) StateRequestHandle(taskID string) (*api.State, error) { 27 | l.Debug("Receive a state request of taskID", taskID) 28 | 29 | taskIDInt, err := strconv.ParseInt(taskID, 10, 64) 30 | if err != nil { 31 | l.Fatal("Error happened when parse int from task ID", taskID, ":", err) 32 | } 33 | l.Debug("Parse task ID to int successfully") 34 | 35 | pState, err := plugins.GetTaskState(taskID) 36 | 37 | // can't get state 38 | if err != nil { 39 | l.Warn("Error happened when get state of task", taskID, ":", err) 40 | // but task id < counter means task finish 41 | if taskIDInt <= atomic.LoadInt64(&counter) { 42 | l.Debug("But task ID <= counter, return finished state") 43 | return api.BuildFinishState(taskID), nil 44 | } 45 | // else, no task fount 46 | l.Error("task ID > counter, return error") 47 | return nil, err 48 | } 49 | if pState.StateCode == api.StateErrorCode { 50 | l.Warn("Get a error state of task", taskID, *pState) 51 | } else { 52 | l.Debug("Get a normal state of task", taskID, *pState) 53 | } 54 | 55 | return pState, nil 56 | } 57 | -------------------------------------------------------------------------------- /plugins/weibo/url.go: -------------------------------------------------------------------------------- 1 | package weibo 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/7sDream/rikka/api" 7 | "github.com/7sDream/rikka/plugins" 8 | ) 9 | 10 | func (wbp weiboPlugin) URLRequestHandle(q *plugins.URLRequest) (pURL *api.URL, err error) { 11 | l.Debug("Receive an url request of task", q.TaskID) 12 | 13 | taskIDInt, err := strconv.ParseInt(q.TaskID, 10, 64) 14 | if err != nil { 15 | l.Fatal("Error happened when parser taskID", q.TaskID, "to int64:", err) 16 | } 17 | l.Debug("Parse int from task ID", q.TaskID, "successfully") 18 | 19 | imageID, ok := imageIDMap[taskIDInt] 20 | if !ok { 21 | l.Fatal("Can't get url of a finished task", q.TaskID) 22 | } 23 | imageURL := buildURL(imageID) 24 | l.Debug("Get image ID", imageID, "successfully, return url", imageURL) 25 | 26 | return &api.URL{URL: imageURL}, nil 27 | } 28 | -------------------------------------------------------------------------------- /plugins/weibo/weibo.go: -------------------------------------------------------------------------------- 1 | package weibo 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | 7 | "github.com/7sDream/rikka/plugins" 8 | ) 9 | 10 | type weiboPlugin struct{} 11 | 12 | var ( 13 | l = plugins.SubLogger("[Weibo]") 14 | 15 | argUpdateCookiesPassword = flag.String( 16 | "ucpwd", "weibo", 17 | "Update cookies password, you need input this password when you visit /cookies to update your cookies", 18 | ) 19 | 20 | client *http.Client 21 | counter int64 22 | 23 | imageIDMap = make(map[int64]string) 24 | 25 | // Plugin is the main plugin object instance 26 | Plugin weiboPlugin 27 | ) 28 | 29 | const ( 30 | cookiesEnvKey = "RIKKA_WEIBO_COOKIES" 31 | ) 32 | -------------------------------------------------------------------------------- /rikkac/README.md: -------------------------------------------------------------------------------- 1 | # Rikkac - CLI tool of Rikka 2 | 3 | [中文版][version-zh] 4 | 5 | Rikkac need to be used with a [Rikka][rikka] server. 6 | 7 | ## Usage 8 | 9 | `rikkac filename` 10 | 11 | `` can be: 12 | 13 | - `-s`: Src, image source url 14 | - `-m`: Markdown 15 | - `-h`: HTML 16 | - `-b`: BBCode 17 | - `-r` reStructuredText 18 | 19 | Src is default format. Format priority as same as the list above, lowest to highest. This is, `-m -b` considered as `-b`, `-m` is ignored. Not so complicated, you shouldn't remember priority if you never provide two format in one command. 20 | 21 | ## Build and Install 22 | 23 | ### Executable Binary Download 24 | 25 | Now we only provide [executable binary for Linux][download], Because I only have Linux installed in my PC, QwQ 26 | 27 | Then rename the file to `rikkac` and move to a folder in your `PATH`. 28 | 29 | OK, installation finished, now you need [configure](#configure-and-usage) Rikkac before use it. 30 | 31 | User of other os please refer to next section to build and install Rikkac. 32 | 33 | ### From Source Code 34 | 35 | First, you need have Golang installed in your PC, then: 36 | 37 | `go get github.com/7sDream/rikka/rikkac` 38 | 39 | Add `$GOPATH/bin` into your `PATH`, if you haven't do this when you install Golang. 40 | 41 | Then run `rikkac --version`, a version number means install successfully. 42 | 43 | You need some [configure](#configure-and-usage) before use Rikkac. 44 | 45 | ## Configure and Usage 46 | 47 | Rikkac need to env variable: `RIKKA_HOST` and `RIKKA_PWD`. for Rikka server address and password. 48 | 49 | ``` 50 | export RIKKA_HOST=https://rikka.7sdre.am 51 | export RIKKA_PWD=afakepassword 52 | ``` 53 | 54 | Then you can enjoy Rikkac. 55 | 56 | Just run `rikkac -m filepath` for upload. 57 | 58 | You can get detail log when you meet some error by add `-v` or `-vv` option. 59 | 60 | ## Multi File upload 61 | 62 | Just provide file path one by one: 63 | 64 | ```bash 65 | rikkac -m file1 file2 file3 ... 66 | ``` 67 | 68 | Or you can use wildcard if your shell support: 69 | 70 | ```bash 71 | rikkac -m *.png 72 | ``` 73 | 74 | ## Tips: Copy Result to Clipboard in Quick 75 | 76 | ```bash 77 | rikkac -m a.png | xclip -sel clip 78 | ``` 79 | 80 | need xclip installed:`apt-get install xclip`. 81 | 82 | [version-zh]: https://github.com/7sDream/rikka/blob/master/rikkac/README.zh.md 83 | 84 | [rikka]: https://github.com/7sDream/rikka 85 | [download]: https://github.com/7sDream/rikka/releases/tag/Rikkac 86 | -------------------------------------------------------------------------------- /rikkac/README.zh.md: -------------------------------------------------------------------------------- 1 | # Rikkac - Rikka 的命令行工具 2 | 3 | [English version][version-en] 4 | 5 | 需要和 [Rikka][rikka] 配合使用。 6 | 7 | ## 使用方式 8 | 9 | `rikkac filename` 10 | 11 | `` 可选的参数如下: 12 | 13 | - `-s`: SRC 图片原始地址 14 | - `-m`: Markdown 格式 15 | - `-h`: HTML 格式 16 | - `-b`: BBCode 格式 17 | - `-r` reStructuredText 格式 18 | 19 | 默认是源地址格式,优先级如上表,从低到高。也就是说下面的会覆盖上面的,`-m -b` 等同于 `-b`。其实也没那么复杂,你只要不同时提供两个就不用记优先级。 20 | 21 | ## 编译安装 22 | 23 | ### 下载二进制文件 24 | 25 | 目前编译好的 Rikkac 工具只提供了 [Linux 版下载][download],因为我这里只有 Linux 系统 QwQ 26 | 27 | 下载了之后重命名为 `rikkac`,放到某个 `PATH` 目录下即可。 28 | 29 | 这样安装就完成了,不过在使用前你还需要进行一些[配置](#配置和使用)。 30 | 31 | 使用其他操作系统的用户请使用下一节所说的从源代码安装。 32 | 33 | ### 从源代码安装 34 | 35 | 首先你需要安装 Go,然后: 36 | 37 | `go get github.com/7sDream/rikka/rikkac` 38 | 39 | 把 `$GOPATH/bin` 加入 `PATH` 如果你在安装 Go 的时候没做这步的话。 40 | 41 | 然后输入 `rikkac --version` 如果输出了一个版本号则说明安装成功了。 42 | 43 | 编译和安装成功后并不能立即使用,需要进行一些[配置](#配置和使用)。 44 | 45 | ## 配置和使用 46 | 47 | Rikkac 需要配置两个环境变量: `RIKKA_HOST` 和 `RIKKA_PWD`。它们分别代表 Rikka 服务器地址和密码。 48 | 49 | ``` 50 | export RIKKA_HOST=https://rikka.7sdre.am 51 | export RIKKA_PWD=afakepassword 52 | ``` 53 | 54 | 配置完就可以使用啦。 55 | 56 | 基本上就是 `rikkac -m filepath` 就好。 57 | 58 | 如果出错了可以用 `-v` 或者 `-vv` 参数输出详细日志用于排错。 59 | 60 | ## 批量上传 61 | 62 | ```bash 63 | rikkac -m file1 file2 file3 ... 64 | ``` 65 | 66 | 这样就行了。 67 | 68 | 如果你用的 shell 带有通配符自动展开的话,那这样也行: 69 | 70 | ```bash 71 | rikkac -m *.png 72 | ``` 73 | 74 | ## 小 tips 快速复制到剪贴板 75 | 76 | ```bash 77 | rikkac -m a.png | xclip -sel clip 78 | ``` 79 | 80 | 此方法需要安装 xclip:`apt-get install xclip`。 81 | 82 | [version-en]: https://github.com/7sDream/rikka/blob/master/rikkac/README.md 83 | 84 | [rikka]: https://github.com/7sDream/rikka/blob/master/README.zh.md 85 | [download]: https://github.com/7sDream/rikka/releases/tag/Rikkac 86 | -------------------------------------------------------------------------------- /rikkac/file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | pathUtil "path/filepath" 7 | "strings" 8 | 9 | "github.com/7sDream/rikka/client" 10 | "github.com/7sDream/rikka/common/util" 11 | ) 12 | 13 | func readFile(filePath string) (string, []byte, error) { 14 | absFilePath, err := pathUtil.Abs(filePath) 15 | if err != nil { 16 | return "", nil, err 17 | } 18 | l.Debug("Change to absolute path:", absFilePath) 19 | 20 | if !util.IsFile(absFilePath) { 21 | return "", nil, errors.New("Path " + absFilePath + " not exists or not a file") 22 | } 23 | l.Debug("File", absFilePath, "exists and is a file") 24 | 25 | fileContent, err := client.CheckFile(absFilePath) 26 | if err != nil { 27 | return "", nil, err 28 | } 29 | return absFilePath, fileContent, nil 30 | } 31 | 32 | func getFile(index int) (string, bool) { 33 | filepath := "" 34 | if len(flag.Args()) > index { 35 | filepath = flag.Args()[index] 36 | if strings.HasPrefix(filepath, "-") { 37 | return filepath, false 38 | } 39 | } else { 40 | return "", false 41 | } 42 | l.Debug("Get path of file want be uploaded:", filepath) 43 | return filepath, true 44 | } 45 | -------------------------------------------------------------------------------- /rikkac/format.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "text/template" 7 | 8 | "github.com/7sDream/rikka/api" 9 | ) 10 | 11 | var ( 12 | templateArgs = []*bool{ 13 | flag.Bool("r", false, "reStructuredText format"), 14 | flag.Bool("b", false, "BBCode format"), 15 | flag.Bool("h", false, "HTML format"), 16 | flag.Bool("m", false, "Markdown format"), 17 | flag.Bool("s", true, "Src url format"), 18 | } 19 | 20 | templateStrings = []string{ 21 | ".. image:: {{ .URL }}", 22 | "[img]{{ .rURL }}[/img]", 23 | "", 24 | "![]({{ .URL }})", 25 | "{{ .URL }}", 26 | } 27 | ) 28 | 29 | func format(url *api.URL) string { 30 | 31 | var templateStr string 32 | 33 | for i, v := range templateArgs { 34 | if *v { 35 | templateStr = templateStrings[i] 36 | break 37 | } 38 | } 39 | 40 | htmlTemplate, err := template.New("_").Parse(templateStr) 41 | if err != nil { 42 | l.Fatal("Error happened when create htmlTemplate with string", templateStr, ":", err) 43 | } 44 | l.Debug("Create htmlTemplate with string", templateStr, "successfully") 45 | 46 | strWriter := bytes.NewBufferString("") 47 | 48 | if err = htmlTemplate.Execute(strWriter, url); err != nil { 49 | l.Fatal("Error happened when execute htmlTemplate :", err) 50 | } 51 | l.Debug("Execute htmlTemplate successfully") 52 | 53 | return strWriter.String() 54 | } 55 | -------------------------------------------------------------------------------- /rikkac/params.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | envHostKey = "RIKKA_HOST" 13 | envPwdKey = "RIKKA_PWD" 14 | ) 15 | 16 | var ( 17 | argHost = flag.String("t", "", "rikka host, will override env variable if not empty") 18 | argPwd = flag.String("p", "", "rikka password, will override env variable if not empty") 19 | ) 20 | 21 | func getHost() string { 22 | 23 | var host string 24 | 25 | if *argHost != "" { 26 | host = *argHost 27 | l.Info("Get host from argument:", host) 28 | } else { 29 | l.Debug("No host argument provided, try get from env") 30 | 31 | host = os.Getenv(envHostKey) 32 | if host == "" { 33 | l.Fatal("No", envHostKey, "env variable, I don't know where to upload") 34 | } 35 | l.Info("Get Rikka host from env:", host) 36 | } 37 | 38 | if !strings.HasPrefix(host, "http") { 39 | host = "http://" + host 40 | l.Debug("Add scheme http:// for host, become:", host) 41 | } else { 42 | l.Debug("Host seems contains scheme, won't process") 43 | } 44 | 45 | if strings.HasSuffix(host, "/") { 46 | host = host[:len(host)-1] 47 | l.Debug("Delete extra / for host, become:", host) 48 | } else { 49 | l.Debug("No extra / in host, won't process") 50 | } 51 | 52 | urlObj, err := url.Parse(host) 53 | if err != nil || urlObj.Host == "" || urlObj.Scheme == "" || urlObj.Path != "" { 54 | l.Fatal("Invalid Rikka host:", host) 55 | } 56 | //noinspection GoNilness because l.Fatal will exit 57 | l.Debug("Host check passed, struct:", fmt.Sprintf("%+v", *urlObj)) 58 | 59 | //noinspection GoNilness, ditto 60 | return urlObj.Scheme + "://" + urlObj.Host 61 | } 62 | 63 | func getPassword() string { 64 | 65 | var password string 66 | 67 | if *argPwd != "" { 68 | password = *argPwd 69 | l.Info("Get password from argument:", password) 70 | } else { 71 | l.Debug("No password argument provided, try get from env") 72 | password = os.Getenv(envPwdKey) 73 | if password == "" { 74 | l.Fatal("No", envPwdKey, "env variable, I don't know password of rikka") 75 | } 76 | l.Info("Get password from env variable:", password) 77 | } 78 | return password 79 | } 80 | -------------------------------------------------------------------------------- /rikkac/rikkac.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/7sDream/rikka/common/logger" 9 | ) 10 | 11 | const ( 12 | version = "0.0.5" 13 | ) 14 | 15 | var ( 16 | l = logger.NewLogger("[Rikkac]") 17 | 18 | argInfo = flag.Bool("v", false, "set logger level to Info") 19 | argDebug = flag.Bool("vv", false, "set logger level to Debug") 20 | argVersion = flag.Bool("version", false, "show rikkac version and exit") 21 | ) 22 | 23 | func init() { 24 | flag.Parse() 25 | 26 | if *argVersion { 27 | fmt.Println(version) 28 | os.Exit(0) 29 | } 30 | 31 | if *argDebug { 32 | logger.SetLevel(logger.LevelDebug) 33 | } else if *argInfo { 34 | logger.SetLevel(logger.LevelInfo) 35 | } else { 36 | logger.SetLevel(logger.LevelWarn) 37 | } 38 | } 39 | 40 | func waitOutput(index int, out chan *taskRes) { 41 | if index == 0 { 42 | l.Fatal("No file provided") 43 | } else if index == 1 { 44 | c := <-out 45 | fmt.Println(c.StringWithoutFilepath()) 46 | } else { 47 | nowShow := 0 48 | resList := make([]*taskRes, index) 49 | for i := 0; i < index; i++ { 50 | c := <-out 51 | resList[c.Index] = c 52 | if c.Index == nowShow { 53 | for nowShow < index && resList[nowShow] != nil { 54 | c = resList[nowShow] 55 | fmt.Println(c.String()) 56 | nowShow++ 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | func main() { 64 | 65 | host := getHost() 66 | 67 | index := 0 68 | ok := true 69 | out := make(chan *taskRes) 70 | var filepath string 71 | 72 | for ok { 73 | filepath, ok = getFile(index) 74 | if ok { 75 | l.Info("Read image file", filepath, "successfully, add to task list") 76 | go worker(host, filepath, index, out) 77 | index++ 78 | } 79 | } 80 | l.Info("End with index", index) 81 | 82 | waitOutput(index, out) 83 | } 84 | -------------------------------------------------------------------------------- /rikkac/worker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/7sDream/rikka/client" 4 | 5 | type taskRes struct { 6 | Index int 7 | Filepath string 8 | Res string 9 | Err error 10 | } 11 | 12 | func (res taskRes) String() string { 13 | if res.Err != nil { 14 | return res.Filepath + ": " + res.Err.Error() 15 | } 16 | return res.Filepath + ": " + res.Res 17 | } 18 | 19 | func (res taskRes) StringWithoutFilepath() string { 20 | if res.Err != nil { 21 | return "Error:" + res.Err.Error() 22 | } 23 | return res.Res 24 | } 25 | 26 | func buildErrorRes(index int, filepath string, err error) *taskRes { 27 | return &taskRes{ 28 | Index: index, 29 | Filepath: filepath, 30 | Err: err, 31 | } 32 | } 33 | 34 | func worker(host string, filepath string, index int, out chan *taskRes) { 35 | absFilepath, fileContent, err := readFile(filepath) 36 | if err != nil { 37 | out <- buildErrorRes(index, filepath, err) 38 | return 39 | } 40 | l.Info("Read file", absFilepath, "successfully") 41 | 42 | taskID, err := client.Upload(host, absFilepath, fileContent, getPassword()) 43 | if err != nil { 44 | out <- buildErrorRes(index, filepath, err) 45 | return 46 | } 47 | l.Info("Upload successfully, get taskID:", taskID) 48 | 49 | err = client.WaitFinish(host, taskID) 50 | if err != nil { 51 | out <- buildErrorRes(index, filepath, err) 52 | return 53 | } 54 | l.Info("Task state comes to finished") 55 | 56 | pURL, err := client.GetURL(host, taskID) 57 | if err != nil { 58 | out <- buildErrorRes(index, filepath, err) 59 | return 60 | } 61 | l.Info("Url gotten:", *pURL) 62 | 63 | formatted := format(pURL) 64 | l.Info("Make final formatted text successfully:", formatted) 65 | 66 | out <- &taskRes{ 67 | Index: index, 68 | Filepath: filepath, 69 | Res: formatted, 70 | Err: nil, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/apiserver/jsonutil.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/7sDream/rikka/api" 9 | "github.com/7sDream/rikka/common/util" 10 | "github.com/7sDream/rikka/plugins" 11 | ) 12 | 13 | // jsonEncode encode a object to json bytes. 14 | func jsonEncode(obj interface{}) ([]byte, error) { 15 | jsonData, err := json.Marshal(obj) 16 | if err == nil { 17 | l.Debug("Encode data", fmt.Sprint(obj), "to json", string(jsonData), "successfully") 18 | return jsonData, nil 19 | } 20 | l.Error("Error happened when encoding", fmt.Sprint(obj), "to json :", err) 21 | return nil, err 22 | } 23 | 24 | // getErrorJson get error json bytes like {"Error": "error message"} 25 | func getErrorJson(err error) ([]byte, error) { 26 | obj := api.Error{ 27 | Error: err.Error(), 28 | } 29 | return jsonEncode(obj) 30 | } 31 | 32 | // getErrorJson get error json bytes like {"TaskId": "12312398374237"} 33 | func getTaskIdJson(taskId string) ([]byte, error) { 34 | obj := api.TaskId{ 35 | TaskId: taskId, 36 | } 37 | return jsonEncode(obj) 38 | } 39 | 40 | // getStateJson get state json bytes. 41 | // Will call plugins.GetState 42 | func getStateJson(taskID string) ([]byte, error) { 43 | l.Debug("Send state request of task", taskID, "to plugin manager") 44 | state, err := plugins.GetState(taskID) 45 | if err != nil { 46 | l.Warn("Error happened when get state of task", taskID, ":", err) 47 | return nil, err 48 | } 49 | l.Debug("Get state of task", taskID, "successfully") 50 | return jsonEncode(state) 51 | } 52 | 53 | // getUrlJson get url json bytes like {"URL": "http://127.0.0.1/files/filename"} 54 | // Will call plugins.GetURL 55 | func getUrlJson(taskID string, r *http.Request, picOp *plugins.ImageOperate) ([]byte, error) { 56 | l.Debug("Send url request of task", taskID, "to plugin manager") 57 | url, err := plugins.GetURL(taskID, r, isServerTLS, picOp) 58 | if err != nil { 59 | l.Error("Error happened when get url of task", taskID, ":", err) 60 | return nil, err 61 | } 62 | l.Debug("Get url of task", taskID, "successfully") 63 | return jsonEncode(url) 64 | } 65 | 66 | func renderErrorJson(w http.ResponseWriter, taskID string, err error, errorCode int) { 67 | errorJSONData, err := getErrorJson(err) 68 | 69 | if util.ErrHandle(w, err) { 70 | // build error json failed 71 | l.Error("Error happened when build error json of task", taskID, ":", err) 72 | return 73 | } 74 | 75 | // build error json successfully 76 | l.Debug("Build error json successfully of task", taskID) 77 | err = util.RenderJson(w, errorJSONData, errorCode) 78 | 79 | if util.ErrHandle(w, err) { 80 | // render error json failed 81 | l.Error("Error happened when render error json", errorJSONData, "of task", taskID, ":", err) 82 | } else { 83 | l.Info("Render error json", string(errorJSONData), "of task", taskID, "successfully") 84 | } 85 | } 86 | 87 | func renderJsonOrError(w http.ResponseWriter, taskID string, jsonData []byte, err error, errorCode int) { 88 | // has error 89 | if err != nil { 90 | renderErrorJson(w, taskID, err, errorCode) 91 | } 92 | 93 | // no error, render json 94 | err = util.RenderJson(w, jsonData, http.StatusOK) 95 | 96 | // render json failed 97 | if util.ErrHandle(w, err) { 98 | l.Error("Error happened when render json", fmt.Sprint(jsonData), "of task", taskID, ":", err) 99 | } else { 100 | l.Debug("Render json", string(jsonData), "of task", taskID, "successfully") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /server/apiserver/start.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/7sDream/rikka/api" 7 | "github.com/7sDream/rikka/common/logger" 8 | "github.com/7sDream/rikka/common/util" 9 | ) 10 | 11 | var ( 12 | password string 13 | maxSizeByMb float64 14 | isServerTLS bool 15 | corsAllowOrigin string 16 | 17 | l *logger.Logger 18 | ) 19 | 20 | func addCors(h http.HandlerFunc) http.HandlerFunc { 21 | return func(w http.ResponseWriter, r *http.Request) { 22 | if len(corsAllowOrigin) > 0 { 23 | w.Header().Set("Access-Control-Allow-Origin", corsAllowOrigin) 24 | } 25 | h(w, r) 26 | } 27 | } 28 | 29 | // StartRikkaAPIServer start API server of Rikka 30 | func StartRikkaAPIServer(argViewPath string, argPassword string, argMaxSizeByMb float64, argIsServerTLS bool, argCorsAllowOrigin string, log *logger.Logger) { 31 | 32 | viewPath = argViewPath 33 | password = argPassword 34 | maxSizeByMb = argMaxSizeByMb 35 | isServerTLS = argIsServerTLS 36 | corsAllowOrigin = argCorsAllowOrigin 37 | 38 | l = log.SubLogger("[API]") 39 | 40 | stateHandler := util.RequestFilter( 41 | "", "GET", l, 42 | util.DisableListDir(l, stateHandleFunc), 43 | ) 44 | 45 | urlHandler := util.RequestFilter( 46 | "", "GET", l, 47 | util.DisableListDir(l, urlHandleFunc), 48 | ) 49 | 50 | uploadHandler := util.RequestFilter( 51 | api.UploadPath, "POST", l, 52 | uploadHandleFunc, 53 | ) 54 | 55 | http.HandleFunc(api.StatePath, addCors(stateHandler)) 56 | http.HandleFunc(api.URLPath, addCors(urlHandler)) 57 | http.HandleFunc(api.UploadPath, addCors(uploadHandler)) 58 | 59 | l.Info("API server start successfully") 60 | } 61 | -------------------------------------------------------------------------------- /server/apiserver/state.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/7sDream/rikka/common/util" 7 | ) 8 | 9 | // stateHandleFunc is the base handle func of path /api/state/taskID 10 | func stateHandleFunc(w http.ResponseWriter, r *http.Request) { 11 | 12 | taskID := util.GetTaskIDByRequest(r) 13 | 14 | l.Info("Receive a state request of task", taskID, "from ip", util.GetClientIP(r)) 15 | 16 | var jsonData []byte 17 | var err error 18 | if jsonData, err = getStateJson(taskID); err != nil { 19 | l.Warn("Error happened when get state json of task", taskID, ":", err) 20 | } else { 21 | l.Debug("Get state json", string(jsonData), "of task", taskID, "successfully") 22 | } 23 | 24 | renderJsonOrError(w, taskID, jsonData, err, http.StatusInternalServerError) 25 | } 26 | -------------------------------------------------------------------------------- /server/apiserver/upload.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "mime/multipart" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/7sDream/rikka/api" 11 | "github.com/7sDream/rikka/common/util" 12 | "github.com/7sDream/rikka/plugins" 13 | ) 14 | 15 | var ( 16 | viewPath = "" 17 | taskIDUploading = "[uploading]" 18 | acceptedTypes = []string{ 19 | "jpeg", "bmp", "gif", "png", 20 | } 21 | ) 22 | 23 | // ---- upload handle aux functions -- 24 | 25 | func checkFromArg(w http.ResponseWriter, r *http.Request, ip string) (string, bool) { 26 | from := r.FormValue(api.FormKeyFrom) 27 | if from != api.FromWebsite && from != api.FromAPI { 28 | l.Warn(ip, "use a error from value:", from) 29 | w.WriteHeader(http.StatusBadRequest) 30 | _, _ = w.Write([]byte(api.InvalidFromArgErrMsg)) 31 | return "", false 32 | } 33 | l.Debug("Request of", ip, "is from:", from) 34 | return from, true 35 | } 36 | 37 | func checkPassword(w http.ResponseWriter, r *http.Request, ip string, from string) bool { 38 | userPassword := r.FormValue(api.FormKeyPWD) 39 | if userPassword != password { 40 | // error password 41 | l.Warn(ip, "input a error password:", userPassword) 42 | 43 | if from == api.FromWebsite { 44 | http.Error(w, "Error password", http.StatusUnauthorized) 45 | } else { 46 | // from == "api" 47 | renderErrorJson(w, taskIDUploading, errors.New(api.ErrPwdErrMsg), http.StatusUnauthorized) 48 | } 49 | 50 | return false 51 | } 52 | l.Debug("Password check for", ip, "successfully") 53 | return true 54 | } 55 | 56 | // IsAccepted check a mime file type is accepted by rikka. 57 | func IsAccepted(fileMimeTypeStr string) (string, bool) { 58 | if !strings.HasPrefix(fileMimeTypeStr, "image") { 59 | return "", false 60 | } 61 | for _, acceptedType := range acceptedTypes { 62 | if strings.HasSuffix(fileMimeTypeStr, "/"+acceptedType) { 63 | return acceptedType, true 64 | } 65 | } 66 | return "", false 67 | } 68 | 69 | func checkUploadedFile(w http.ResponseWriter, file multipart.File, ip string, from string) (*plugins.SaveRequest, bool) { 70 | fileContent, err := ioutil.ReadAll(file) 71 | if err != nil { 72 | l.Error("Error happened when get form file content of ip", ip, ":", err) 73 | 74 | if from == api.FromWebsite { 75 | util.ErrHandle(w, err) 76 | } else { 77 | // from == "api" 78 | renderErrorJson(w, taskIDUploading, err, http.StatusInternalServerError) 79 | } 80 | 81 | return nil, false 82 | } 83 | l.Debug("Get form file content of ip", ip, "successfully") 84 | 85 | fileType := http.DetectContentType(fileContent) 86 | 87 | ext, ok := IsAccepted(fileType) 88 | 89 | if !ok { 90 | l.Error("Form file submitted by", ip, "is not a image, it is a", fileType) 91 | 92 | if from == api.FromWebsite { 93 | util.ErrHandle(w, errors.New(api.NotAImgFileErrMsg)) 94 | } else { 95 | // from == "api" 96 | renderErrorJson(w, taskIDUploading, errors.New(api.NotAImgFileErrMsg), http.StatusInternalServerError) 97 | } 98 | 99 | return nil, false 100 | } 101 | l.Debug("Check type of form file submitted by", ip, ", passed:", fileType) 102 | 103 | if _, err = file.Seek(0, 0); err != nil { 104 | l.Error("Error when try to seek form file submitted by", ip, "to start:", err) 105 | 106 | if from == api.FromWebsite { 107 | util.ErrHandle(w, err) 108 | } else { 109 | // from == "api" 110 | renderErrorJson(w, taskIDUploading, err, http.StatusInternalServerError) 111 | } 112 | 113 | return nil, false 114 | } 115 | 116 | return &plugins.SaveRequest{ 117 | File: file, 118 | FileSize: int64(len(fileContent)), 119 | FileExt: ext, 120 | }, true 121 | } 122 | 123 | func getUploadedFile(w http.ResponseWriter, r *http.Request, ip string, from string) (*plugins.SaveRequest, bool) { 124 | file, _, err := r.FormFile(api.FormKeyFile) 125 | if err != nil { 126 | // no needed file 127 | l.Error("Error happened when get form file from request of", ip, ":", err) 128 | 129 | if from == api.FromWebsite { 130 | util.ErrHandle(w, err) 131 | } else { 132 | // from == "api" 133 | renderErrorJson(w, taskIDUploading, err, http.StatusBadRequest) 134 | } 135 | 136 | return nil, false 137 | } 138 | l.Debug("Get uploaded file from request of", ip, "successfully") 139 | 140 | pSaveRequest, ok := checkUploadedFile(w, file, ip, from) 141 | if !ok { 142 | return nil, false 143 | } 144 | 145 | return pSaveRequest, true 146 | } 147 | 148 | func redirectToView(w http.ResponseWriter, r *http.Request, ip string, taskID string) { 149 | viewPage := viewPath + taskID 150 | http.Redirect(w, r, viewPage, http.StatusFound) 151 | l.Debug("Redirect client", ip, "to view page", viewPage) 152 | } 153 | 154 | func sendSaveRequestToPlugin(w http.ResponseWriter, pStateRequest *plugins.SaveRequest, ip string, from string) (string, bool) { 155 | l.Debug("Send file save request to plugin manager") 156 | 157 | pTaskID, err := plugins.AcceptFile(pStateRequest) 158 | 159 | if err != nil { 160 | l.Error("Error happened when plugin manager process file save request by ip", ip, ":", err) 161 | if from == api.FromWebsite { 162 | util.ErrHandle(w, err) 163 | } else { 164 | renderErrorJson(w, taskIDUploading, err, http.StatusInternalServerError) 165 | } 166 | return "", false 167 | } 168 | 169 | taskID := pTaskID.TaskId 170 | l.Info("Receive task ID request by", ip, "from plugin manager:", taskID) 171 | 172 | return taskID, true 173 | } 174 | 175 | func sendUploadResultToClient(w http.ResponseWriter, r *http.Request, ip string, taskID string, from string) { 176 | if from == api.FromWebsite { 177 | redirectToView(w, r, ip, taskID) 178 | } else { 179 | var taskIdJSON []byte 180 | var err error 181 | if taskIdJSON, err = getTaskIdJson(taskID); err != nil { 182 | l.Error("Error happened when build task ID json of task", taskID, "request by", ip, ":", err) 183 | } else { 184 | l.Info("Build task ID json", taskIdJSON, "of task", "request by", ip, "successfully") 185 | } 186 | renderJsonOrError(w, taskID, taskIdJSON, err, http.StatusInternalServerError) 187 | } 188 | } 189 | 190 | // ---- end of upload handle aux functions -- 191 | 192 | func uploadHandleFunc(w http.ResponseWriter, r *http.Request) { 193 | ip := util.GetClientIP(r) 194 | 195 | l.Info("Receive file upload request from ip", ip) 196 | 197 | maxSize := int64(maxSizeByMb * 1024 * 1024) 198 | r.Body = http.MaxBytesReader(w, r.Body, maxSize) 199 | 200 | err := r.ParseMultipartForm(maxSize) 201 | if util.ErrHandleWithCode(w, err, http.StatusBadRequest) { 202 | l.Error("Error happened when parse form submitted by", ip, ":", err) 203 | return 204 | } 205 | 206 | var from string 207 | var ok bool 208 | if from, ok = checkFromArg(w, r, ip); !ok { 209 | return 210 | } 211 | 212 | if !checkPassword(w, r, ip, from) { 213 | return 214 | } 215 | 216 | var pSaveRequest *plugins.SaveRequest 217 | if pSaveRequest, ok = getUploadedFile(w, r, ip, from); !ok { 218 | return 219 | } 220 | 221 | var taskID string 222 | if taskID, ok = sendSaveRequestToPlugin(w, pSaveRequest, ip, from); !ok { 223 | return 224 | } 225 | 226 | sendUploadResultToClient(w, r, ip, taskID, from) 227 | } 228 | -------------------------------------------------------------------------------- /server/apiserver/url.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/7sDream/rikka/common/util" 7 | ) 8 | 9 | func urlHandleFunc(w http.ResponseWriter, r *http.Request) { 10 | ip := util.GetClientIP(r) 11 | taskID := util.GetTaskIDByRequest(r) 12 | 13 | l.Info("Receive a url request of task", taskID, "from ip", ip) 14 | 15 | var jsonData []byte 16 | var err error 17 | 18 | if jsonData, err = getUrlJson(taskID, r, nil); err != nil { 19 | l.Error("Error happened when get url json of task", taskID, "request by", ip, ":", err) 20 | } else { 21 | l.Debug("Get url json", string(jsonData), "of task", taskID, "request by", ip, "successfully") 22 | } 23 | 24 | renderJsonOrError(w, taskID, jsonData, err, http.StatusInternalServerError) 25 | } 26 | -------------------------------------------------------------------------------- /server/start.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | pathUtil "path/filepath" 6 | 7 | "github.com/7sDream/rikka/common/logger" 8 | "github.com/7sDream/rikka/common/util" 9 | "github.com/7sDream/rikka/server/apiserver" 10 | "github.com/7sDream/rikka/server/webserver" 11 | ) 12 | 13 | var ( 14 | l = logger.NewLogger("[Server]") 15 | ) 16 | 17 | // StartRikka start server part of rikka. Include Web Server and API server. 18 | func StartRikka(socket string, password string, maxSizeByMb float64, https bool, certDir string, allowOrigin string) { 19 | realHttps := false 20 | 21 | certPemPath := pathUtil.Join(certDir, "cert.pem") 22 | keyPemPath := pathUtil.Join(certDir, "key.pem") 23 | 24 | if https { 25 | if util.IsFile(certPemPath) && util.IsFile(keyPemPath) { 26 | realHttps = true 27 | } else { 28 | l.Warn("Cert dir argument is not a valid dir, fallback to http") 29 | } 30 | } 31 | 32 | l.Info("Start web server...") 33 | viewPath := webserver.StartRikkaWebServer(maxSizeByMb, https, l) 34 | 35 | l.Info("Start API server...") 36 | apiserver.StartRikkaAPIServer(viewPath, password, maxSizeByMb, https, allowOrigin, l) 37 | 38 | l.Info("Rikka is listening", socket) 39 | 40 | // real http server function call 41 | var err error 42 | if realHttps { 43 | err = http.ListenAndServeTLS(socket, certPemPath, keyPemPath, nil) 44 | } else { 45 | err = http.ListenAndServe(socket, nil) 46 | } 47 | 48 | if err != nil { 49 | l.Fatal("Error when try listening", socket, ":", err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/webserver/check.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "flag" 5 | "github.com/7sDream/rikka/common/util" 6 | 7 | pathUtil "path/filepath" 8 | ) 9 | 10 | var argWebServerRootDir = flag.String( 11 | "wsroot", "server/webserver", 12 | "Root dir of rikka web server, templates should be saved in wsroot/templates "+ 13 | "and static files should be saved in wsroot/static", 14 | ) 15 | 16 | var ( 17 | staticDirName = "static" 18 | templateDirName = "templates" 19 | 20 | webServerRootDirPath = "" // will get from cli argument 21 | staticDirPath = "" // append webServerRootDirPath with staticDirName 22 | templateDirPath = "" // append webServerRootDirPath with templateDirName 23 | 24 | homeTemplateFileName = "index.html" 25 | viewTemplateFileName = "view.html" 26 | finishedViewTemplateFileName = "viewFinish.html" 27 | 28 | homeTemplateFilePath = "" 29 | viewTemplateFilePath = "" 30 | finishedViewTemplateFilePath = "" 31 | 32 | staticFilesName = []string{ 33 | "css", 34 | "css/common.css", "css/index.css", "css/view.css", 35 | "js", 36 | "js/copy.js", "js/getSrc.js", "js/onError.js", "js/checkForm.js", 37 | "image", 38 | "image/rikka.png", "image/favicon.png", 39 | } 40 | ) 41 | 42 | // updatePathVars update module level path var 43 | func updatePathVars(root string) { 44 | webServerRootDirPath = root 45 | staticDirPath = pathUtil.Join(webServerRootDirPath, staticDirName) 46 | templateDirPath = pathUtil.Join(webServerRootDirPath, templateDirName) 47 | 48 | homeTemplateFilePath = pathUtil.Join(templateDirPath, homeTemplateFileName) 49 | viewTemplateFilePath = pathUtil.Join(templateDirPath, viewTemplateFileName) 50 | finishedViewTemplateFilePath = pathUtil.Join(templateDirPath, finishedViewTemplateFileName) 51 | } 52 | 53 | // calcRequireFileList calc file list that web server require 54 | func calcRequireFileList(root string) []string { 55 | 56 | updatePathVars(root) 57 | 58 | requireFiles := []string{ 59 | webServerRootDirPath, 60 | 61 | templateDirPath, 62 | homeTemplateFilePath, viewTemplateFilePath, finishedViewTemplateFilePath, 63 | 64 | staticDirPath, 65 | } 66 | 67 | for _, filename := range staticFilesName { 68 | requireFiles = append(requireFiles, pathUtil.Join(staticDirPath, filename)) 69 | } 70 | 71 | return requireFiles 72 | } 73 | 74 | // Check needed files like html, css, js, logo, etc... 75 | func checkFiles() { 76 | l.Info("Args wsroot = ", *argWebServerRootDir) 77 | 78 | absWebServerDir, err := pathUtil.Abs(*argWebServerRootDir) 79 | if err != nil { 80 | l.Fatal("Provided web server root dir", *argWebServerRootDir, "is a invalid path") 81 | } 82 | l.Debug("Change web server dir to absolute:", absWebServerDir) 83 | 84 | l.Info("Check needed files") 85 | 86 | for _, filepath := range calcRequireFileList(absWebServerDir) { 87 | if !util.CheckExist(filepath) { 88 | l.Fatal(filepath, "not exist, check failed") 89 | } else { 90 | l.Debug("File", filepath, "exist, check passed") 91 | } 92 | } 93 | 94 | l.Info("Check needed files successfully") 95 | } 96 | -------------------------------------------------------------------------------- /server/webserver/context.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import "github.com/7sDream/rikka/api" 4 | 5 | var ( 6 | context = struct { 7 | Version string 8 | RootPath string 9 | UploadPath string 10 | StaticPath string 11 | FavIconPath string 12 | MaxSizeByMb float64 13 | TaskID string 14 | URL string 15 | FormKeyFile string 16 | FormKeyPWD string 17 | FormKeyFrom string 18 | FromWebsite string 19 | }{ 20 | Version: api.Version, 21 | RootPath: RootPath, 22 | UploadPath: api.UploadPath, 23 | StaticPath: StaticPath, 24 | FavIconPath: "", 25 | MaxSizeByMb: 0, 26 | TaskID: "", 27 | URL: "", 28 | FormKeyFile: api.FormKeyFile, 29 | FormKeyPWD: api.FormKeyPWD, 30 | FormKeyFrom: api.FormKeyFrom, 31 | FromWebsite: api.FromWebsite, 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /server/webserver/index.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/7sDream/rikka/common/util" 7 | ) 8 | 9 | // IndexHandler handle request ask for homepage(${RootPath}, "/" in general), use templates/index.html 10 | // Only accept GET method. 11 | func indexHandlerGenerator() http.HandlerFunc { 12 | return util.RequestFilter( 13 | RootPath, "GET", l, 14 | util.TemplateRenderHandler( 15 | homeTemplateFilePath, 16 | func(r *http.Request) interface{} { return context }, l, 17 | ), 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /server/webserver/path.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | // Can be configured 4 | const ( 5 | RootPath = "/" 6 | ViewSuffix = "view/" 7 | StaticSuffix = "static/" 8 | faviconFileName = "image/favicon.png" 9 | ) 10 | 11 | // Available path of web server, DON'T configure those 12 | const ( 13 | ViewPath = RootPath + ViewSuffix 14 | StaticPath = RootPath + StaticSuffix 15 | FavIconOriginPath = "/favicon.ico" 16 | FavIconTruePath = StaticPath + faviconFileName 17 | ) 18 | -------------------------------------------------------------------------------- /server/webserver/start.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/7sDream/rikka/common/logger" 7 | ) 8 | 9 | var ( 10 | isServeTLS bool 11 | l *logger.Logger 12 | ) 13 | 14 | // StartRikkaWebServer start web server of rikka. 15 | func StartRikkaWebServer(maxSizeByMb float64, argIsServeTLS bool, log *logger.Logger) string { 16 | 17 | if maxSizeByMb <= 0 { 18 | l.Fatal("Max file size can't be equal or less than 0, you set it to", maxSizeByMb) 19 | } 20 | 21 | isServeTLS = argIsServeTLS 22 | 23 | context.MaxSizeByMb = maxSizeByMb 24 | context.FavIconPath = FavIconTruePath 25 | 26 | l = log.SubLogger("[Web]") 27 | 28 | checkFiles() 29 | 30 | http.HandleFunc(RootPath, indexHandlerGenerator()) 31 | http.HandleFunc(ViewPath, viewHandleGenerator()) 32 | http.HandleFunc(StaticPath, staticFsHandlerGenerator()) 33 | http.HandleFunc(FavIconOriginPath, favIconHandlerGenerator()) 34 | 35 | l.Info("Rikka web server start successfully") 36 | 37 | return ViewPath 38 | } 39 | -------------------------------------------------------------------------------- /server/webserver/static.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/7sDream/rikka/common/util" 7 | ) 8 | 9 | // The static file server handle all request that ask for files under static dir, from url path {StaticPath} 10 | // Only accept GET method 11 | func staticFsHandlerGenerator() http.HandlerFunc { 12 | return util.RequestFilter( 13 | "", "GET", l, 14 | util.DisableListDir( 15 | l, 16 | http.StripPrefix( 17 | StaticPath[:len(StaticPath)-1], 18 | http.FileServer(http.Dir(staticDirPath)), 19 | ).ServeHTTP, 20 | ), 21 | ) 22 | } 23 | 24 | func favIconHandlerGenerator() http.HandlerFunc { 25 | return util.RequestFilter( 26 | FavIconOriginPath, "GET", l, 27 | http.RedirectHandler(FavIconTruePath, http.StatusPermanentRedirect).ServeHTTP, 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /server/webserver/static/css/common.css: -------------------------------------------------------------------------------- 1 | /*noinspection CssUnknownTarget*/ 2 | @import 'https://fonts.googleapis.com/css?family=Satisfy'; 3 | .hide { 4 | display: none; 5 | } 6 | 7 | div#error { 8 | color: #6cf; 9 | margin-bottom: 1em; 10 | font-family: monospace; 11 | padding: 0.5em; 12 | border: 0.1em solid #6cf; 13 | border-radius: 0.2em; 14 | } 15 | 16 | div#error > span { 17 | font-family: monospace; 18 | } 19 | 20 | h1, p, span { 21 | font-family: 'Satisfy', sans-serif; 22 | } 23 | 24 | body, header, main { 25 | display: flex; 26 | flex-flow: column nowrap; 27 | justify-content: center; 28 | align-items: center; 29 | } 30 | 31 | header { 32 | margin-bottom: 2em; 33 | } 34 | 35 | h1 { 36 | order: 0; 37 | font-size: 3em; 38 | margin: 0; 39 | } 40 | 41 | h1 a { 42 | text-decoration: none; 43 | color: black; 44 | } 45 | 46 | header p { 47 | font-size: 1em; 48 | margin: 0; 49 | } 50 | 51 | main { 52 | margin-bottom: 1em; 53 | } 54 | 55 | img.preview { 56 | margin-bottom: 2em; 57 | } 58 | 59 | form { 60 | display: flex; 61 | flex-flow: column nowrap; 62 | align-items: center; 63 | justify-content: flex-start; 64 | } 65 | 66 | div.line { 67 | display: flex; 68 | margin-bottom: 0.5em; 69 | width: 15em; 70 | height: 1.6em; 71 | } 72 | 73 | div.line label, div.line input { 74 | color: #66ccff; 75 | border: 1px solid #6cf; 76 | border-radius: 2px; 77 | text-align: center; 78 | } 79 | 80 | div.line label { 81 | font-size: 1em; 82 | width: 40%; 83 | } 84 | 85 | div.line input { 86 | font-size: 0.75em; 87 | width: 60%; 88 | outline: none; 89 | } 90 | 91 | footer { 92 | width: 100%; 93 | font-size: 1em; 94 | display: flex; 95 | flex-flow: column nowrap; 96 | justify-content: center; 97 | align-items: center; 98 | } 99 | 100 | footer p { 101 | margin-bottom: 0; 102 | } 103 | -------------------------------------------------------------------------------- /server/webserver/static/css/index.css: -------------------------------------------------------------------------------- 1 | img.logo { 2 | margin-bottom: 1em; 3 | } 4 | 5 | @keyframes rotate { 6 | from { 7 | transform: rotate(0); 8 | } 9 | to { 10 | transform: rotate(360deg); 11 | } 12 | } 13 | 14 | img#uploading { 15 | width: 24px; 16 | height: 24px; 17 | margin-bottom: 1em; 18 | visibility: hidden; 19 | } 20 | 21 | img#uploading.show { 22 | content: url('data:image/gif;base64,R0lGODlhGAAYAPYHALjn/mbM//r8/vX7/mnN/u74/ofX/rzo/uD0/o7Z/vz9/oLV/u/5/mrN/nXR/pLa/vn8/sbr/obW/vL6/pTb/sjs/m3O/nvT/qDf/s7u/qfh/o3Y/vb7/r/p/m7O/nnS/oDU/pjc/sPq/uP1/nzT/pHa/rHk/pvd/nfR/uz4/rTl/pXb/uf2/tbx/qTg/ovY/tzy/n/U/nDP/sDp/oPV/qri/tHv/rLl/qPg/s/u/nTQ/ur3/qvi/tLv/nHP/rbm/srt/vP6/svt/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQFBQAHACwAAAAAGAAYAAAHmoAHgoOEhYaHgwMzKxIEASAhIhyIhAIAMgGZmpkeKgKUEwabo5k+BYgMH6SkNogKoqubNZQAsZsGCogcPraZMimUQL2ZQpQHK8MuxgcxvSCfxpixFizLB9KxLdbNtg5By8i9D8sVwwEdxhzYsQ0jxrXDz5QKEubKlAwo5q2UBbC2powJ+LFuk4cb0IwN6ECBRqYLKzpMskaxUCAAIfkEBQUAHAAsCwABAAoAFgAABUkgZ0lUBXFomlqAoqrBwrxoEFzFawfJANsaXaARoQUcCKNhYsQIjAda6nGSxnJWpJQTsGSsNgA4gHEJAxufEGeU0VhmDkHyqLxCACH5BAUFAAoALAsAAQALABYAAAZNQEWAFBJxFMik0qNSOpOFp/IjTQYS1YD24NQGCK6UUgsWj8tPtFMtNbSqyMALBteWEFntapQPnFhSXgBZL3AoZk8giEsmAmMoKx0DTkEAIfkEBQUABgAsCwABAAwAFgAABlNAgwEkhAiPSKEsyRTSmtCogaaQGgDRQMAz0Hq/WhHYS+LZIF/dKcJANh6AUdSByxmzhBJgl9V+aj0KY2Adg15chlomUgsCVlIFUTdVSTEhMxxMQQAh+QQJBQAFACwLAAEADAAWAAAGRkACQbKaDQrIpBIJECyfhsnzyZgqDdbs1McJeL/gwCxMXpHDqDO4of562t4YPGCGj9uybhuglWgLVVZYVjI/Tks0FB1HS0EAIfkECQUABgAsCwABAAwAFgAABmRAQgAUEkEMyGRguZSpFEkDc0orKKfMj1WKZdKg3SmAGw54BmWmqEzi2SBT3SnCiDYegFE0GnDgckd7ZAEEJQA7fGEfNT0KaUsdj2ePJoNYCwKWTCgpSGEgW5YeN1BXMSEzHIJBACH5BAkFAAwALAsAAQALABYAAAZXQEKAFBJxGEhkYLn0qBRJpnRRUEqZn+pVmmBspYdvgOBKbckp6xKd9I7LbSQ7rjS06OoXjH4tIdpfKyNqWycsYksAiC9uWyhpXyBpjU0mAlEBKCsdA3FBACH5BAUFAB0ALAsAAQAKABYAAAVPIGFJVAV1aBesqwUoKCsvjCqzV3GzybAHmpisEUnJHIiULWCYKG0YwVN1mMYep+lqUdCukE+ZJaPcAYw7DOwX2Ph+uR9t2XrFCJJHhaMMAQA7'); 23 | animation: rotate 4s linear infinite; 24 | visibility: inherit; 25 | } 26 | 27 | input#uploadFile { 28 | position: absolute; 29 | top: 0; 30 | right: 0; 31 | height: 0; 32 | width: 0; 33 | margin: 0; 34 | padding: 0; 35 | opacity: 0; 36 | display: none; 37 | filter: alpha(opacity=0); 38 | } 39 | 40 | label#fakeUploadBtn { 41 | cursor: pointer; 42 | } 43 | 44 | input#submitBtn { 45 | color: #66ccff; 46 | border: 1px solid; 47 | border-radius: 2px; 48 | text-align: center; 49 | font-size: 1em; 50 | background: white; 51 | line-height: 1.5em; 52 | width: 50%; 53 | font-weight: bold; 54 | } 55 | -------------------------------------------------------------------------------- /server/webserver/static/css/view.css: -------------------------------------------------------------------------------- 1 | img.preview { 2 | height: 268px; 3 | } 4 | 5 | p#state { 6 | font-family: monospace; 7 | margin-bottom: 3em; 8 | } 9 | -------------------------------------------------------------------------------- /server/webserver/static/image/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7sDream/rikka/80b44f55ea10f02e7b3506ee001f68367fa9a341/server/webserver/static/image/favicon.png -------------------------------------------------------------------------------- /server/webserver/static/image/rikka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7sDream/rikka/80b44f55ea10f02e7b3506ee001f68367fa9a341/server/webserver/static/image/rikka.png -------------------------------------------------------------------------------- /server/webserver/static/js/checkForm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isImageType(typeStr) { 4 | // noinspection JSUnresolvedFunction 5 | if (typeStr.startsWith("image") === false) { 6 | return false; 7 | } 8 | 9 | const accepted = ["jpeg", "bmp", "gif", "png"]; 10 | return accepted.some(function(type){ 11 | // noinspection JSUnresolvedFunction 12 | return typeStr.endsWith("/" + type) 13 | }) 14 | } 15 | 16 | function check(maxSizeByMb) { 17 | const passwordInput = document.querySelector("input#password"); 18 | const fileInput = document.querySelector("input#uploadFile"); 19 | 20 | const file = fileInput.files[0]; 21 | 22 | if (passwordInput.value === "") { 23 | alert("Please input password"); 24 | return false; 25 | } 26 | 27 | if (file === undefined) { 28 | alert("Please choose a image to upload"); 29 | return false; 30 | } 31 | 32 | let fileType = file.type; 33 | if (!isImageType(fileType)) { 34 | fileType = fileType || "unknown"; 35 | alert("Can't upload a " + fileType + " type file"); 36 | return false; 37 | } 38 | 39 | if (file.size > (maxSizeByMb * 1024 * 1024)) { 40 | const fileSizeByMb = Math.round(file.size / 1024 / 1024 * 100) / 100; 41 | alert("Max file size is " + maxSizeByMb + " Mb, input file is " + fileSizeByMb.toString() + " Mb"); 42 | return false; 43 | } 44 | 45 | const imgElement = document.querySelector("img#uploading"); 46 | imgElement.classList.add("show"); 47 | 48 | return true; 49 | } 50 | -------------------------------------------------------------------------------- /server/webserver/static/js/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function addCopyEventListener(url){ 4 | const divs = document.querySelectorAll("div.copyAsText"); 5 | 6 | for (const index in divs) { 7 | 8 | if(!divs.hasOwnProperty(index)) { 9 | continue; 10 | } 11 | 12 | const div = divs[index]; 13 | const input = div.querySelector("input"); 14 | const btn = div.querySelector("label"); 15 | 16 | if (url !== "") { 17 | const template = input.getAttribute("data-template"); 18 | input.value = template.replace("${url}", url); 19 | } 20 | 21 | void function(btn, input) { 22 | btn.addEventListener("click", function(){ 23 | if (btn.disabled) { 24 | return; 25 | } 26 | 27 | let res = false; 28 | try { 29 | const section = window.getSelection(); 30 | section.removeAllRanges(); 31 | 32 | input.disabled = false; 33 | 34 | input.focus(); 35 | input.setSelectionRange(0, input.value.length); 36 | 37 | res = document.execCommand("copy"); 38 | 39 | input.disabled = true 40 | } catch(e) { 41 | res = false; 42 | } 43 | if (res) { 44 | const origin = btn.textContent; 45 | 46 | btn.textContent = "Copied!"; 47 | btn.disabled = true; 48 | 49 | setTimeout(function(){ 50 | btn.textContent = origin; 51 | btn.disabled = false; 52 | }, 2000); 53 | } else { 54 | window.prompt("Copy to clipboard: Ctrl+C, Enter", input.value); 55 | } 56 | }); 57 | }(btn, input); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/webserver/static/js/getSrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function AJAX(method, url) { 4 | // noinspection JSUnresolvedFunction 5 | return new Promise(function (resolve, reject){ 6 | 7 | const req = new XMLHttpRequest(); 8 | 9 | req.onload = function () { 10 | if (req.status === 200) { 11 | resolve(req.response); 12 | } else { 13 | let errorMsg = req.response; 14 | try { 15 | // noinspection JSCheckFunctionSignatures 16 | const errorJson = JSON.parse(errorMsg); 17 | errorMsg = errorJson["Error"]; 18 | } catch (e) { 19 | errorMsg = e.message; 20 | } 21 | reject(new Error(errorMsg)); 22 | } 23 | }; 24 | 25 | req.onerror = function(){ reject(new Error("Network error.")); }; 26 | 27 | req.open(method, url, true); 28 | req.send(); 29 | }); 30 | } 31 | 32 | function hide(elem) { 33 | elem.classList.add("hide"); 34 | } 35 | 36 | function show(elem) { 37 | elem.classList.remove("hide"); 38 | } 39 | 40 | function getPhotoState(taskID, times) { 41 | 42 | times = times || 0; 43 | 44 | const stateElement = document.querySelector("p#state"); 45 | const imageElement = document.querySelector("img.preview"); 46 | const formElement = document.querySelector("form"); 47 | 48 | // noinspection JSUnresolvedFunction 49 | AJAX("GET", "/api/state/" + taskID).then(function (res) { 50 | 51 | const json = JSON.parse(res); 52 | 53 | if ("Error" in json) { 54 | throw new Error(json["Error"]); 55 | } 56 | 57 | const state = json['StateCode']; 58 | 59 | if (state === -1) { // Error state 60 | throw new Error(json['Description']); 61 | } else if (state === 0) { // Successful state 62 | return AJAX("GET", '/api/url/' + json["TaskID"]); 63 | } else { // Other state 64 | stateElement.textContent = "Request " + times.toString() + ", upload state: " + json['Description'] + ", please wait..."; 65 | setTimeout(getPhotoState, 1000, taskID, times + 1); 66 | // noinspection JSUnresolvedFunction 67 | return new Promise(function(){}); 68 | } 69 | 70 | }).then(function(res){ 71 | 72 | const json = JSON.parse(res); 73 | 74 | if ("Error" in json) { 75 | throw new Error(json["Error"]); 76 | } 77 | 78 | return json["URL"]; 79 | 80 | }).then(function (url){ 81 | 82 | imageElement.src = url; 83 | 84 | hide(stateElement); 85 | show(imageElement); 86 | show(formElement); 87 | 88 | addCopyEventListener(url); 89 | 90 | }).catch(function(err){ 91 | 92 | stateElement.textContent = "Error: " + err.message + ", please close page"; 93 | 94 | show(stateElement); 95 | hide(imageElement); 96 | hide(formElement); 97 | 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /server/webserver/static/js/onError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function errorHandler(message, source, lineNumber, colNumber, error) { 4 | 5 | const errorDiv = document.getElementById("error"); 6 | 7 | errorDiv.classList.remove("hide"); 8 | 9 | console.log("Error happened, message:", message); 10 | console.log("On source file: ", source); 11 | console.log("On line - col: ", lineNumber, "-", colNumber); 12 | console.log("Error:", error); 13 | 14 | try { 15 | const ua = navigator.userAgent; 16 | console.log("UA: ", ua); 17 | } catch (e) { 18 | console.log("Unable to get UA"); 19 | } 20 | 21 | } 22 | 23 | window.onerror = errorHandler; 24 | -------------------------------------------------------------------------------- /server/webserver/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rikka - Home - {{ .Version }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 | 13 |
Some error happened on this page, please open console of your browser and send a snapshot to developers, thanks!
14 | 15 |
16 |

RIKKA

17 |

A minimalist personal image sharing website

18 |
19 | 20 |
21 | 22 | 23 |
25 |
26 | 27 | 28 |
29 |
30 |
31 | 32 | 33 |
34 | 35 | 36 |
37 |
38 | 39 |
40 |

Version {{ .Version }}

41 |

Design with , by 7sDream

42 |
43 | 44 | 45 | 52 | 53 | -------------------------------------------------------------------------------- /server/webserver/templates/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rikka - {{ .Version }} - View - {{ .TaskID }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 | 13 |
Some error happened on this page, please open console of your browser and send a snapshot to developers, thanks!
14 | 15 |
16 |

RIKKA

17 |

A minimalist personal image sharing website

18 |
19 |
20 | 21 |

State: Getting image state, please wait. If you see this message more than 10 seconds, please refresh.

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 |

Version {{ .Version}}

51 |

Design with , by 7sDream

52 |
53 | 54 | 55 | 56 | 57 | 61 | 62 | -------------------------------------------------------------------------------- /server/webserver/templates/viewFinish.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rikka - {{ .Version }} - View - {{ .TaskID }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 | 13 |
Some error happened on this page, please open console of your browser and send a snapshot to developers, thanks!
14 | 15 |
16 |

RIKKA

17 |

A minimalist personal image sharing website

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 |

Version {{ .Version}}

49 |

Design with , by 7sDream

50 |
51 | 52 | 53 | 57 | 58 | -------------------------------------------------------------------------------- /server/webserver/view.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/7sDream/rikka/api" 7 | "github.com/7sDream/rikka/common/util" 8 | "github.com/7sDream/rikka/plugins" 9 | ) 10 | 11 | func viewHandleFunc(w http.ResponseWriter, r *http.Request) { 12 | taskID := util.GetTaskIDByRequest(r) 13 | context.TaskID = taskID 14 | ip := util.GetClientIP(r) 15 | 16 | l.Info("Receive a view request of task", taskID, "from ip", ip) 17 | 18 | l.Debug("Send a url request of task", taskID, "to plugin manager") 19 | 20 | var pURL *api.URL 21 | var err error 22 | if pURL, err = plugins.GetURL(taskID, r, isServeTLS, nil); err != nil { 23 | // state is not finished or error when get url, use view.html 24 | templateFilePath := viewTemplateFilePath 25 | l.Warn("Can't get url of task", taskID, ":", err) 26 | l.Warn("Render template", viewTemplateFileName) 27 | err = util.RenderTemplate(templateFilePath, w, context) 28 | if util.ErrHandle(w, err) { 29 | // RenderTemplate error 30 | l.Error("Error happened when render template", viewTemplateFileName, "to", ip, ":", err) 31 | } else { 32 | // successfully 33 | l.Info("Render template", viewTemplateFileName, "to", ip, "successfully") 34 | } 35 | return 36 | } 37 | 38 | // state is finished, use viewFinish.html 39 | l.Debug("Receive url of task", taskID, ":", pURL.URL) 40 | templateFilePath := finishedViewTemplateFilePath 41 | context.URL = pURL.URL 42 | err = util.RenderTemplate(templateFilePath, w, context) 43 | if util.ErrHandle(w, err) { 44 | // RenderTemplate error 45 | l.Error("Error happened when render template", finishedViewTemplateFileName, "to", ip, ":", err) 46 | } else { 47 | // successfully 48 | l.Info("Render template", finishedViewTemplateFileName, "to", ip, "successfully") 49 | } 50 | } 51 | 52 | // ViewHandler handle request ask for image view page(${ViewPath}), use templates/view.html 53 | // Only accept GET Method 54 | func viewHandleGenerator() http.HandlerFunc { 55 | return util.RequestFilter( 56 | "", "GET", l, 57 | util.DisableListDir(l, viewHandleFunc), 58 | ) 59 | } 60 | --------------------------------------------------------------------------------