diff --git a/.babelrc b/.babelrc index a2a58cffcc..4595f77284 100644 --- a/.babelrc +++ b/.babelrc @@ -1,13 +1,13 @@ -{ - "presets": [ - ["env", { "modules": false }], - "stage-2" - ], - "plugins": ["transform-runtime"], - "env": { - "test": { - "presets": ["env", "stage-2"], - "plugins": [ "istanbul" ] - } - } -} +{ + "presets": [ + ["env", { "modules": false }], + "stage-2" + ], + "plugins": ["transform-runtime"], + "env": { + "test": { + "presets": ["env", "stage-2"], + "plugins": [ "istanbul" ] + } + } +} diff --git a/.dockerignore b/.dockerignore index 8f664b3d7b..ac45b0ea50 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ -assets/ -testdata/ -caddy/ -.github/ +assets/ +testdata/ +caddy/ +.github/ diff --git a/.editorconfig b/.editorconfig index a34668e540..2043e3a3ed 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,14 +1,14 @@ -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -# 4 space indentation -[*.go] -indent_style = tab +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# 4 space indentation +[*.go] +indent_style = tab indent_size = 4 \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 34af3774f3..c8cd769090 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -build/*.js -config/*.js +build/*.js +config/*.js diff --git a/.eslintrc.js b/.eslintrc.js index 67c085d604..74d59e4f56 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,27 +1,27 @@ -// http://eslint.org/docs/user-guide/configuring - -module.exports = { - root: true, - parser: 'babel-eslint', - parserOptions: { - sourceType: 'module' - }, - env: { - browser: true, - }, - // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style - extends: 'standard', - // required to lint *.vue files - plugins: [ - 'html' - ], - // add your custom rules here - 'rules': { - // allow paren-less arrow functions - 'arrow-parens': 0, - // allow async-await - 'generator-star-spacing': 0, - // allow debugger during development - 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 - } -} +// http://eslint.org/docs/user-guide/configuring + +module.exports = { + root: true, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module' + }, + env: { + browser: true, + }, + // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style + extends: 'standard', + // required to lint *.vue files + plugins: [ + 'html' + ], + // add your custom rules here + 'rules': { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + } +} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index ad3cfc9a93..f361c61a89 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,24 +1,24 @@ -### Instructions (remove before submitting): - -1. Are you asking for help with using Caddy or File Manager? Please use our forum instead: https://forum.caddyserver.com. -2. If you are filing a bug report, please answer the following questions. -3. If your issue is not a bug report, you do not need to use this template. -4. If not using with Caddy, ignore questions 1 and 2. - -### 1. Have you downloaded File Manager from caddyserver.com? If yes, when have you done that? If no, and you are running a custom build, which is the revision of File Manager's repository? - -### 2. What is your entire Caddyfile? -```text -(Put Caddyfile here) -``` - -### 3. What are you trying to do? - - -### 4. What did you expect to see? - - -### 5. What did you see instead (give full error messages and/or log)? - - -### 6. How can someone who is starting from scratch reproduce this behaviour as minimally as possible? +### Instructions (remove before submitting): + +1. Are you asking for help with using Caddy or File Manager? Please use our forum instead: https://forum.caddyserver.com. +2. If you are filing a bug report, please answer the following questions. +3. If your issue is not a bug report, you do not need to use this template. +4. If not using with Caddy, ignore questions 1 and 2. + +### 1. Have you downloaded File Manager from caddyserver.com? If yes, when have you done that? If no, and you are running a custom build, which is the revision of File Manager's repository? + +### 2. What is your entire Caddyfile? +```text +(Put Caddyfile here) +``` + +### 3. What are you trying to do? + + +### 4. What did you expect to see? + + +### 5. What did you see instead (give full error messages and/or log)? + + +### 6. How can someone who is starting from scratch reproduce this behaviour as minimally as possible? diff --git a/.goreleaser.yml b/.goreleaser.yml index 6231c26b49..779b22222f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,29 +1,29 @@ -build: - main: cmd/filemanager/main.go - binary: filemanager - goos: - - darwin - - linux - - windows - - freebsd - - netbsd - - openbsd - goarch: - - amd64 - - 386 - - arm - - arm64 - ignore: - - goos: openbsd - goarch: arm - goarm: 6 - - goos: freebsd - goarch: arm - goarm: 6 - -archive: - name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}" - format: tar.gz - format_overrides: - - goos: windows - format: zip +build: + main: cmd/filemanager/main.go + binary: filemanager + goos: + - darwin + - linux + - windows + - freebsd + - netbsd + - openbsd + goarch: + - amd64 + - 386 + - arm + - arm64 + ignore: + - goos: openbsd + goarch: arm + goarm: 6 + - goos: freebsd + goarch: arm + goarm: 6 + +archive: + name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}" + format: tar.gz + format_overrides: + - goos: windows + format: zip diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f0cc78e8a5..883952d356 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,46 +1,46 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hacdias@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hacdias@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe47ed8eae..813a4fb191 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,14 @@ -# Contributing - -If you want to contribute or want to build the code from source, you will need to have the most recent version of Go and, if you want to change the static assets (JS, CSS, ...), Node.js installed on your computer. To start developing, you just need to do the following: - -1. `go get github.com/hacdias/filemanager` -2. `cd $GOPATH/src/github.com/hacdias/filemanager` -3. `npm install` -4. `npm run dev` - regenerates the static assets automatically -5. `go install github.com/hacdias/filemanager/cmd/filemanager` -6. Execute `$GOPATH/bin/filemanager` - -The steps 3 and 4 are only required **if you want to develop the front-end**. Otherwise, you can ignore them. Before pulling, if you made any change on assets folder, you must run the `build.sh` script on the root of this repository. - -If you are using this as a Caddy plugin, you should use its [official instructions for plugins](https://github.com/mholt/caddy/wiki/Extending-Caddy#2-plug-in-your-plugin) and import `github.com/hacdias/filemanager/caddy/filemanager`. +# Contributing + +If you want to contribute or want to build the code from source, you will need to have the most recent version of Go and, if you want to change the static assets (JS, CSS, ...), Node.js installed on your computer. To start developing, you just need to do the following: + +1. `go get github.com/hacdias/filemanager` +2. `cd $GOPATH/src/github.com/hacdias/filemanager` +3. `npm install` +4. `npm run dev` - regenerates the static assets automatically +5. `go install github.com/hacdias/filemanager/cmd/filemanager` +6. Execute `$GOPATH/bin/filemanager` + +The steps 3 and 4 are only required **if you want to develop the front-end**. Otherwise, you can ignore them. Before pulling, if you made any change on assets folder, you must run the `build.sh` script on the root of this repository. + +If you are using this as a Caddy plugin, you should use its [official instructions for plugins](https://github.com/mholt/caddy/wiki/Extending-Caddy#2-plug-in-your-plugin) and import `github.com/hacdias/filemanager/caddy/filemanager`. diff --git a/Dockerfile b/Dockerfile index ca67698099..d5d3b8e7c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,22 @@ -FROM golang:alpine - -COPY . /go/src/github.com/hacdias/filemanager - -WORKDIR /go/src/github.com/hacdias/filemanager -RUN apk add --no-cache git -RUN go get ./... - -WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager -RUN CGO_ENABLED=0 go build -a -RUN mv filemanager /go/bin/filemanager - -FROM scratch -COPY --from=0 /go/bin/filemanager /filemanager - -VOLUME /srv -EXPOSE 80 - -COPY Docker.json /config.json - -ENTRYPOINT ["/filemanager"] -CMD ["--config", "/config.json"] +FROM golang:alpine + +COPY . /go/src/github.com/hacdias/filemanager + +WORKDIR /go/src/github.com/hacdias/filemanager +RUN apk add --no-cache git +RUN go get ./... + +WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager +RUN CGO_ENABLED=0 go build -a +RUN mv filemanager /go/bin/filemanager + +FROM scratch +COPY --from=0 /go/bin/filemanager /filemanager + +VOLUME /srv +EXPOSE 80 + +COPY Docker.json /config.json + +ENTRYPOINT ["/filemanager"] +CMD ["--config", "/config.json"] diff --git a/LICENSE.md b/LICENSE.md index 5e0fd33cbb..48d49423d5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,201 +1,201 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by -the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all -other entities that control, are controlled by, or are under common -control with that entity. For the purposes of this definition, -"control" means (i) the power, direct or indirect, to cause the -direction or management of such entity, whether by contract or -otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity -exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation -source, and configuration files. - -"Object" form shall mean any form resulting from mechanical -transformation or translation of a Source form, including but -not limited to compiled object code, generated documentation, -and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or -Object form, made available under the License, as indicated by a -copyright notice that is included in or attached to the work -(an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object -form, that is based on (or derived from) the Work and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. For the purposes -of this License, Derivative Works shall not include works that remain -separable from, or merely link (or bind by name) to the interfaces of, -the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including -the original version of the Work and any modifications or additions -to that Work or Derivative Works thereof, that is intentionally -submitted to Licensor for inclusion in the Work by the copyright owner -or by an individual or Legal Entity authorized to submit on behalf of -the copyright owner. For the purposes of this definition, "submitted" -means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, -and issue tracking systems that are managed by, or on behalf of, the -Licensor for the purpose of discussing and improving the Work, but -excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity -on behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the -Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of -this License, each Contributor hereby grants to You a perpetual, -worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, -use, offer to sell, sell, import, and otherwise transfer the Work, -where such license applies only to those patent claims licensable -by such Contributor that are necessarily infringed by their -Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You -institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work -or a Contribution incorporated within the Work constitutes direct -or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the -Work or Derivative Works thereof in any medium, with or without -modifications, and in Source or Object form, provided that You -meet the following conditions: - -(a) You must give any other recipients of the Work or -Derivative Works a copy of this License; and - -(b) You must cause any modified files to carry prominent notices -stating that You changed the files; and - -(c) You must retain, in the Source form of any Derivative Works -that You distribute, all copyright, patent, trademark, and -attribution notices from the Source form of the Work, -excluding those notices that do not pertain to any part of -the Derivative Works; and - -(d) If the Work includes a "NOTICE" text file as part of its -distribution, then any Derivative Works that You distribute must -include a readable copy of the attribution notices contained -within such NOTICE file, excluding those notices that do not -pertain to any part of the Derivative Works, in at least one -of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or -documentation, if provided along with the Derivative Works; or, -within a display generated by the Derivative Works, if and -wherever such third-party notices normally appear. The contents -of the NOTICE file are for informational purposes only and -do not modify the License. You may add Your own attribution -notices within Derivative Works that You distribute, alongside -or as an addendum to the NOTICE text from the Work, provided -that such additional attribution notices cannot be construed -as modifying the License. - -You may add Your own copyright statement to Your modifications and -may provide additional or different license terms and conditions -for use, reproduction, or distribution of Your modifications, or -for any such Derivative Works as a whole, provided Your use, -reproduction, and distribution of the Work otherwise complies with -the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work -by You to the Licensor shall be under the terms and conditions of -this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify -the terms of any separate license agreement you may have executed -with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, -except as required for reasonable and customary use in describing the -origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or -agreed to in writing, Licensor provides the Work (and each -Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied, including, without limitation, any warranties or conditions -of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A -PARTICULAR PURPOSE. You are solely responsible for determining the -appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, -whether in tort (including negligence), contract, or otherwise, -unless required by applicable law (such as deliberate and grossly -negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a -result of this License or out of the use or inability to use the -Work (including but not limited to damages for loss of goodwill, -work stoppage, computer failure or malfunction, or any and all -other commercial damages or losses), even if such Contributor -has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing -the Work or Derivative Works thereof, You may choose to offer, -and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this -License. However, in accepting such obligations, You may act only -on Your own behalf and on Your sole responsibility, not on behalf -of any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason -of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following -boilerplate notice, with the fields enclosed by brackets "{}" -replaced with your own identifying information. (Don't include -the brackets!) The text should be enclosed in the appropriate -comment syntax for the file format. We also recommend that a -file or class name and description of purpose be included on the -same "printed page" as the copyright notice for easier -identification within third-party archives. - -Copyright {yyyy} {name of copyright owner} - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "{}" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright {yyyy} {name of copyright owner} + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index d548c397e2..35d0088b89 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,78 @@ -![Preview](https://user-images.githubusercontent.com/5447088/28537288-39be4288-70a2-11e7-8ce9-0813d59f46b7.gif) - -# filemanager - -[![Build](https://img.shields.io/travis/hacdias/filemanager.svg?style=flat-square)](https://travis-ci.org/hacdias/filemanager) -[![Go Report Card](https://goreportcard.com/badge/github.com/hacdias/filemanager?style=flat-square)](https://goreportcard.com/report/hacdias/filemanager) -[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/hacdias/filemanager) - -filemanager provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app or as a middleware. - -# Table of contents - -+ [Getting started](#getting-started) -+ [Features](#features) - - [Users](#users) - - [Search](#search) -+ [Contributing](#contributing) -+ [Donate](#donate) - -# Getting started - -You can find the Getting Started guide on the [documentation](https://henriquedias.com/filemanager/quick-start/). - -# Features - -Easy login system. - -![Login Page](https://user-images.githubusercontent.com/5447088/28432382-975493dc-6d7f-11e7-9190-23f8037159dc.jpg) - -Listings of your files, available in two styles: mosaic and list. You can delete, move, rename, upload and create new files, as well as directories. Single files can be downloaded directly, and multiple files as *.zip*, *.tar*, *.tar.gz*, *.tar.bz2* or *.tar.xz*. - -![Mosaic Listing](https://user-images.githubusercontent.com/5447088/28432384-9771bb4c-6d7f-11e7-8564-3a9bd6a3ce3a.jpg) - -File Manager editor is powered by [Codemirror](https://codemirror.net/) and if you're working with markdown files with metadata, both parts will be separated from each other so you can focus on the content. - -![Markdown Editor](https://user-images.githubusercontent.com/5447088/28432383-9756fdac-6d7f-11e7-8e58-fec49470d15f.jpg) - -On the settings page, a regular user can set its own custom CSS to personalize the experience and change its password. For admins, they can manage the permissions of each user, set commands which can be executed when certain events are triggered (such as before saving and after saving) and change plugin's settings. - -![Settings](https://user-images.githubusercontent.com/5447088/28432385-9776ec66-6d7f-11e7-90a5-891bacd4d02f.jpg) - -We also allow the users to search in the directories and execute commands if allowed. - -## Users - -We support multiple users and each user can have its own scope and custom stylesheet. The administrator is able to choose which permissions should be given to the users, as well as the commands they can execute. Each user also have a set of rules, in which he can be prevented or allowed to access some directories (regular expressions included!). - -![Users](https://user-images.githubusercontent.com/5447088/28432386-977f388a-6d7f-11e7-9006-87d16f05f1f8.jpg) - -## Search - -FileManager allows you to search through your files and it has some options. By default, your search will be something like this: - -``` -this are keywords -``` - -If you search for that it will look at every file that contains "this", "are" or "keywords" on their name. If you want to search for an exact term, you should surround your search by double quotes: - -``` -"this is the name" -``` - -That will search for any file that contains "this is the name" on its name. It won't search for each separated term this time. - -By default, every search will be case sensitive. Although, you can make a case insensitive search by adding `case:insensitive` to the search terms, like this: - -``` -this are keywords case:insensitive -``` - -# Contributing - -The contributing guidelines can be found [here](https://github.com/hacdias/filemanager/blob/master/CONTRIBUTING.md). - -# Donate - -Enjoying this project? You can [donate to its creator](https://henriquedias.com/donate/). He will appreciate. +![Preview](https://user-images.githubusercontent.com/5447088/28537288-39be4288-70a2-11e7-8ce9-0813d59f46b7.gif) + +# filemanager + +[![Build](https://img.shields.io/travis/hacdias/filemanager.svg?style=flat-square)](https://travis-ci.org/hacdias/filemanager) +[![Go Report Card](https://goreportcard.com/badge/github.com/hacdias/filemanager?style=flat-square)](https://goreportcard.com/report/hacdias/filemanager) +[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/hacdias/filemanager) + +filemanager provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app or as a middleware. + +# Table of contents + ++ [Getting started](#getting-started) ++ [Features](#features) + - [Users](#users) + - [Search](#search) ++ [Contributing](#contributing) ++ [Donate](#donate) + +# Getting started + +You can find the Getting Started guide on the [documentation](https://henriquedias.com/filemanager/quick-start/). + +# Features + +Easy login system. + +![Login Page](https://user-images.githubusercontent.com/5447088/28432382-975493dc-6d7f-11e7-9190-23f8037159dc.jpg) + +Listings of your files, available in two styles: mosaic and list. You can delete, move, rename, upload and create new files, as well as directories. Single files can be downloaded directly, and multiple files as *.zip*, *.tar*, *.tar.gz*, *.tar.bz2* or *.tar.xz*. + +![Mosaic Listing](https://user-images.githubusercontent.com/5447088/28432384-9771bb4c-6d7f-11e7-8564-3a9bd6a3ce3a.jpg) + +File Manager editor is powered by [Codemirror](https://codemirror.net/) and if you're working with markdown files with metadata, both parts will be separated from each other so you can focus on the content. + +![Markdown Editor](https://user-images.githubusercontent.com/5447088/28432383-9756fdac-6d7f-11e7-8e58-fec49470d15f.jpg) + +On the settings page, a regular user can set its own custom CSS to personalize the experience and change its password. For admins, they can manage the permissions of each user, set commands which can be executed when certain events are triggered (such as before saving and after saving) and change plugin's settings. + +![Settings](https://user-images.githubusercontent.com/5447088/28432385-9776ec66-6d7f-11e7-90a5-891bacd4d02f.jpg) + +We also allow the users to search in the directories and execute commands if allowed. + +## Users + +We support multiple users and each user can have its own scope and custom stylesheet. The administrator is able to choose which permissions should be given to the users, as well as the commands they can execute. Each user also have a set of rules, in which he can be prevented or allowed to access some directories (regular expressions included!). + +![Users](https://user-images.githubusercontent.com/5447088/28432386-977f388a-6d7f-11e7-9006-87d16f05f1f8.jpg) + +## Search + +FileManager allows you to search through your files and it has some options. By default, your search will be something like this: + +``` +this are keywords +``` + +If you search for that it will look at every file that contains "this", "are" or "keywords" on their name. If you want to search for an exact term, you should surround your search by double quotes: + +``` +"this is the name" +``` + +That will search for any file that contains "this is the name" on its name. It won't search for each separated term this time. + +By default, every search will be case sensitive. Although, you can make a case insensitive search by adding `case:insensitive` to the search terms, like this: + +``` +this are keywords case:insensitive +``` + +# Contributing + +The contributing guidelines can be found [here](https://github.com/hacdias/filemanager/blob/master/CONTRIBUTING.md). + +# Donate + +Enjoying this project? You can [donate to its creator](https://henriquedias.com/donate/). He will appreciate. diff --git a/assets/build/build.js b/assets/build/build.js index c7c0fbdb31..431f3c8f8f 100644 --- a/assets/build/build.js +++ b/assets/build/build.js @@ -1,31 +1,31 @@ -require('./check-versions')() - -process.env.NODE_ENV = 'production' - -var ora = require('ora') -var rm = require('rimraf') -var path = require('path') -var chalk = require('chalk') -var webpack = require('webpack') -var config = require('./config') -var webpackConfig = require('./webpack.prod.conf') - -var spinner = ora('building for production...') -spinner.start() - -rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => { - if (err) throw err - webpack(webpackConfig, function (err, stats) { - spinner.stop() - if (err) throw err - process.stdout.write(stats.toString({ - colors: true, - modules: false, - children: false, - chunks: false, - chunkModules: false - }) + '\n\n') - - console.log(chalk.cyan(' Build complete.\n')) - }) -}) +require('./check-versions')() + +process.env.NODE_ENV = 'production' + +var ora = require('ora') +var rm = require('rimraf') +var path = require('path') +var chalk = require('chalk') +var webpack = require('webpack') +var config = require('./config') +var webpackConfig = require('./webpack.prod.conf') + +var spinner = ora('building for production...') +spinner.start() + +rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => { + if (err) throw err + webpack(webpackConfig, function (err, stats) { + spinner.stop() + if (err) throw err + process.stdout.write(stats.toString({ + colors: true, + modules: false, + children: false, + chunks: false, + chunkModules: false + }) + '\n\n') + + console.log(chalk.cyan(' Build complete.\n')) + }) +}) diff --git a/assets/build/check-versions.js b/assets/build/check-versions.js index f9133e2409..57bcf30a04 100644 --- a/assets/build/check-versions.js +++ b/assets/build/check-versions.js @@ -1,48 +1,48 @@ -var chalk = require('chalk') -var semver = require('semver') -var packageConfig = require('../../package.json') -var shell = require('shelljs') -function exec (cmd) { - return require('child_process').execSync(cmd).toString().trim() -} - -var versionRequirements = [ - { - name: 'node', - currentVersion: semver.clean(process.version), - versionRequirement: packageConfig.engines.node - } -] - -if (shell.which('npm')) { - versionRequirements.push({ - name: 'npm', - currentVersion: exec('npm --version'), - versionRequirement: packageConfig.engines.npm - }) -} - -module.exports = function () { - var warnings = [] - for (var i = 0; i < versionRequirements.length; i++) { - var mod = versionRequirements[i] - if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { - warnings.push(mod.name + ': ' + - chalk.red(mod.currentVersion) + ' should be ' + - chalk.green(mod.versionRequirement) - ) - } - } - - if (warnings.length) { - console.log('') - console.log(chalk.yellow('To use this template, you must update following to modules:')) - console.log() - for (var i = 0; i < warnings.length; i++) { - var warning = warnings[i] - console.log(' ' + warning) - } - console.log() - process.exit(1) - } -} +var chalk = require('chalk') +var semver = require('semver') +var packageConfig = require('../../package.json') +var shell = require('shelljs') +function exec (cmd) { + return require('child_process').execSync(cmd).toString().trim() +} + +var versionRequirements = [ + { + name: 'node', + currentVersion: semver.clean(process.version), + versionRequirement: packageConfig.engines.node + } +] + +if (shell.which('npm')) { + versionRequirements.push({ + name: 'npm', + currentVersion: exec('npm --version'), + versionRequirement: packageConfig.engines.npm + }) +} + +module.exports = function () { + var warnings = [] + for (var i = 0; i < versionRequirements.length; i++) { + var mod = versionRequirements[i] + if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { + warnings.push(mod.name + ': ' + + chalk.red(mod.currentVersion) + ' should be ' + + chalk.green(mod.versionRequirement) + ) + } + } + + if (warnings.length) { + console.log('') + console.log(chalk.yellow('To use this template, you must update following to modules:')) + console.log() + for (var i = 0; i < warnings.length; i++) { + var warning = warnings[i] + console.log(' ' + warning) + } + console.log() + process.exit(1) + } +} diff --git a/assets/build/config.js b/assets/build/config.js index 885ac29d8f..19e9071884 100644 --- a/assets/build/config.js +++ b/assets/build/config.js @@ -1,26 +1,26 @@ -// see http://vuejs-templates.github.io/webpack for documentation. -var path = require('path') - -module.exports = { - index: path.resolve(__dirname, '../dist/index.html'), - assetsRoot: path.resolve(__dirname, '../dist'), - assetsSubDirectory: 'static', - assetsPublicPath: '{{ .BaseURL }}/', - build: { - env: { - NODE_ENV: '"production"' - }, - productionSourceMap: true, - // Run the build command with an extra argument to - // View the bundle analyzer report after build finishes: - // `npm run build --report` - // Set to `true` or `false` to always turn it on or off - bundleAnalyzerReport: process.env.npm_config_report - }, - dev: { - env: { - NODE_ENV: '"development"' - }, - produceSourceMap: true - } -} +// see http://vuejs-templates.github.io/webpack for documentation. +var path = require('path') + +module.exports = { + index: path.resolve(__dirname, '../dist/index.html'), + assetsRoot: path.resolve(__dirname, '../dist'), + assetsSubDirectory: 'static', + assetsPublicPath: '{{ .BaseURL }}/', + build: { + env: { + NODE_ENV: '"production"' + }, + productionSourceMap: true, + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report + }, + dev: { + env: { + NODE_ENV: '"development"' + }, + produceSourceMap: true + } +} diff --git a/assets/build/service-worker-dev.js b/assets/build/service-worker-dev.js index 979e1962a0..5b17a57998 100644 --- a/assets/build/service-worker-dev.js +++ b/assets/build/service-worker-dev.js @@ -1,17 +1,17 @@ -// This service worker file is effectively a 'no-op' that will reset any -// previous service worker registered for the same host:port combination. -// In the production build, this file is replaced with an actual service worker -// file that will precache your site's local assets. -// See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 - -self.addEventListener('install', () => self.skipWaiting()); - -self.addEventListener('activate', () => { - self.clients.matchAll({ type: 'window' }).then(windowClients => { - for (let windowClient of windowClients) { - // Force open pages to refresh, so that they have a chance to load the - // fresh navigation response from the local dev server. - windowClient.navigate(windowClient.url); - } - }); +// This service worker file is effectively a 'no-op' that will reset any +// previous service worker registered for the same host:port combination. +// In the production build, this file is replaced with an actual service worker +// file that will precache your site's local assets. +// See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 + +self.addEventListener('install', () => self.skipWaiting()); + +self.addEventListener('activate', () => { + self.clients.matchAll({ type: 'window' }).then(windowClients => { + for (let windowClient of windowClients) { + // Force open pages to refresh, so that they have a chance to load the + // fresh navigation response from the local dev server. + windowClient.navigate(windowClient.url); + } + }); }); \ No newline at end of file diff --git a/assets/build/service-worker-prod.js b/assets/build/service-worker-prod.js index 1179ec20f9..bf704fdaf6 100644 --- a/assets/build/service-worker-prod.js +++ b/assets/build/service-worker-prod.js @@ -1,55 +1,55 @@ -(function() { - 'use strict'; - - // Check to make sure service workers are supported in the current browser, - // and that the current page is accessed from a secure origin. Using a - // service worker from an insecure origin will trigger JS console errors. - const isLocalhost = Boolean(window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.1/8 is considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) - ); - - window.addEventListener('load', function() { - if ('serviceWorker' in navigator && - (window.location.protocol === 'https:' || isLocalhost)) { - navigator.serviceWorker.register('{{ .BaseURL }}/sw.js') - .then(function(registration) { - // updatefound is fired if service-worker.js changes. - registration.onupdatefound = function() { - // updatefound is also fired the very first time the SW is installed, - // and there's no need to prompt for a reload at that point. - // So check here to see if the page is already controlled, - // i.e. whether there's an existing service worker. - if (navigator.serviceWorker.controller) { - // The updatefound event implies that registration.installing is set - const installingWorker = registration.installing; - - installingWorker.onstatechange = function() { - switch (installingWorker.state) { - case 'installed': - // At this point, the old content will have been purged and the - // fresh content will have been added to the cache. - // It's the perfect time to display a "New content is - // available; please refresh." message in the page's interface. - break; - - case 'redundant': - throw new Error('The installing ' + - 'service worker became redundant.'); - - default: - // Ignore - } - }; - } - }; - }).catch(function(e) { - console.error('Error during service worker registration:', e); - }); - } - }); -})(); +(function() { + 'use strict'; + + // Check to make sure service workers are supported in the current browser, + // and that the current page is accessed from a secure origin. Using a + // service worker from an insecure origin will trigger JS console errors. + const isLocalhost = Boolean(window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) + ); + + window.addEventListener('load', function() { + if ('serviceWorker' in navigator && + (window.location.protocol === 'https:' || isLocalhost)) { + navigator.serviceWorker.register('{{ .BaseURL }}/sw.js') + .then(function(registration) { + // updatefound is fired if service-worker.js changes. + registration.onupdatefound = function() { + // updatefound is also fired the very first time the SW is installed, + // and there's no need to prompt for a reload at that point. + // So check here to see if the page is already controlled, + // i.e. whether there's an existing service worker. + if (navigator.serviceWorker.controller) { + // The updatefound event implies that registration.installing is set + const installingWorker = registration.installing; + + installingWorker.onstatechange = function() { + switch (installingWorker.state) { + case 'installed': + // At this point, the old content will have been purged and the + // fresh content will have been added to the cache. + // It's the perfect time to display a "New content is + // available; please refresh." message in the page's interface. + break; + + case 'redundant': + throw new Error('The installing ' + + 'service worker became redundant.'); + + default: + // Ignore + } + }; + } + }; + }).catch(function(e) { + console.error('Error during service worker registration:', e); + }); + } + }); +})(); diff --git a/assets/build/utils.js b/assets/build/utils.js index 616ceb3a93..09f8272a8f 100644 --- a/assets/build/utils.js +++ b/assets/build/utils.js @@ -1,70 +1,70 @@ -var path = require('path') -var config = require('./config') -var ExtractTextPlugin = require('extract-text-webpack-plugin') - -exports.assetsPath = function (_path) { - var assetsSubDirectory = config.assetsSubDirectory - - return path.posix.join(assetsSubDirectory, _path) -} - -exports.cssLoaders = function (options) { - options = options || {} - - var cssLoader = { - loader: 'css-loader', - options: { - minimize: process.env.NODE_ENV === 'production', - sourceMap: options.sourceMap - } - } - - // generate loader string to be used with extract text plugin - function generateLoaders (loader, loaderOptions) { - var loaders = [cssLoader] - if (loader) { - loaders.push({ - loader: loader + '-loader', - options: Object.assign({}, loaderOptions, { - sourceMap: options.sourceMap - }) - }) - } - - // Extract CSS when that option is specified - // (which is the case during production build) - if (options.extract) { - return ExtractTextPlugin.extract({ - use: loaders, - fallback: 'vue-style-loader' - }) - } else { - return ['vue-style-loader'].concat(loaders) - } - } - - // https://vue-loader.vuejs.org/en/configurations/extract-css.html - return { - css: generateLoaders(), - postcss: generateLoaders(), - less: generateLoaders('less'), - sass: generateLoaders('sass', { indentedSyntax: true }), - scss: generateLoaders('sass'), - stylus: generateLoaders('stylus'), - styl: generateLoaders('stylus') - } -} - -// Generate loaders for standalone style files (outside of .vue) -exports.styleLoaders = function (options) { - var output = [] - var loaders = exports.cssLoaders(options) - for (var extension in loaders) { - var loader = loaders[extension] - output.push({ - test: new RegExp('\\.' + extension + '$'), - use: loader - }) - } - return output -} +var path = require('path') +var config = require('./config') +var ExtractTextPlugin = require('extract-text-webpack-plugin') + +exports.assetsPath = function (_path) { + var assetsSubDirectory = config.assetsSubDirectory + + return path.posix.join(assetsSubDirectory, _path) +} + +exports.cssLoaders = function (options) { + options = options || {} + + var cssLoader = { + loader: 'css-loader', + options: { + minimize: process.env.NODE_ENV === 'production', + sourceMap: options.sourceMap + } + } + + // generate loader string to be used with extract text plugin + function generateLoaders (loader, loaderOptions) { + var loaders = [cssLoader] + if (loader) { + loaders.push({ + loader: loader + '-loader', + options: Object.assign({}, loaderOptions, { + sourceMap: options.sourceMap + }) + }) + } + + // Extract CSS when that option is specified + // (which is the case during production build) + if (options.extract) { + return ExtractTextPlugin.extract({ + use: loaders, + fallback: 'vue-style-loader' + }) + } else { + return ['vue-style-loader'].concat(loaders) + } + } + + // https://vue-loader.vuejs.org/en/configurations/extract-css.html + return { + css: generateLoaders(), + postcss: generateLoaders(), + less: generateLoaders('less'), + sass: generateLoaders('sass', { indentedSyntax: true }), + scss: generateLoaders('sass'), + stylus: generateLoaders('stylus'), + styl: generateLoaders('stylus') + } +} + +// Generate loaders for standalone style files (outside of .vue) +exports.styleLoaders = function (options) { + var output = [] + var loaders = exports.cssLoaders(options) + for (var extension in loaders) { + var loader = loaders[extension] + output.push({ + test: new RegExp('\\.' + extension + '$'), + use: loader + }) + } + return output +} diff --git a/assets/build/vue-loader.conf.js b/assets/build/vue-loader.conf.js index a17529fc1c..8953aaca57 100644 --- a/assets/build/vue-loader.conf.js +++ b/assets/build/vue-loader.conf.js @@ -1,12 +1,12 @@ -var utils = require('./utils') -var config = require('./config') -var isProduction = process.env.NODE_ENV === 'production' - -module.exports = { - loaders: utils.cssLoaders({ - sourceMap: isProduction - ? config.build.productionSourceMap - : config.dev.produceSourceMap, - extract: isProduction - }) -} +var utils = require('./utils') +var config = require('./config') +var isProduction = process.env.NODE_ENV === 'production' + +module.exports = { + loaders: utils.cssLoaders({ + sourceMap: isProduction + ? config.build.productionSourceMap + : config.dev.produceSourceMap, + extract: isProduction + }) +} diff --git a/assets/build/webpack.base.conf.js b/assets/build/webpack.base.conf.js index f818802259..53c97bfec4 100644 --- a/assets/build/webpack.base.conf.js +++ b/assets/build/webpack.base.conf.js @@ -1,69 +1,69 @@ -var path = require('path') -var utils = require('./utils') -var config = require('./config') -var vueLoaderConfig = require('./vue-loader.conf') - -function resolve (dir) { - return path.join(__dirname, '..', dir) -} - -module.exports = { - entry: { - app: './assets/src/main.js' - }, - output: { - path: config.assetsRoot, - filename: '[name].js', - publicPath: config.assetsPublicPath - }, - resolve: { - extensions: ['.js', '.vue', '.json'], - alias: { - 'vue$': 'vue/dist/vue.esm.js', - '@': resolve('src') - } - }, - module: { - rules: [ - { - test: /\.(yml|yaml)$/, - loader: 'yml-loader' - }, - { - test: /\.(js|vue)$/, - loader: 'eslint-loader', - enforce: 'pre', - include: [resolve('src'), resolve('test')], - options: { - formatter: require('eslint-friendly-formatter') - } - }, - { - test: /\.vue$/, - loader: 'vue-loader', - options: vueLoaderConfig - }, - { - test: /\.js$/, - loader: 'babel-loader', - include: [resolve('src'), resolve('test')] - }, - { - test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, - loader: 'url-loader', - options: { - limit: 10000, - name: utils.assetsPath('img/[name].[hash:7].[ext]') - } - }, - { - test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, - loader: 'url-loader', - options: { - // limit: 10000, - name: utils.assetsPath('fonts/[name].[hash:7].[ext]') - } - } - ] - } -} +var path = require('path') +var utils = require('./utils') +var config = require('./config') +var vueLoaderConfig = require('./vue-loader.conf') + +function resolve (dir) { + return path.join(__dirname, '..', dir) +} + +module.exports = { + entry: { + app: './assets/src/main.js' + }, + output: { + path: config.assetsRoot, + filename: '[name].js', + publicPath: config.assetsPublicPath + }, + resolve: { + extensions: ['.js', '.vue', '.json'], + alias: { + 'vue$': 'vue/dist/vue.esm.js', + '@': resolve('src') + } + }, + module: { + rules: [ + { + test: /\.(yml|yaml)$/, + loader: 'yml-loader' + }, + { + test: /\.(js|vue)$/, + loader: 'eslint-loader', + enforce: 'pre', + include: [resolve('src'), resolve('test')], + options: { + formatter: require('eslint-friendly-formatter') + } + }, + { + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig + }, + { + test: /\.js$/, + loader: 'babel-loader', + include: [resolve('src'), resolve('test')] + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('img/[name].[hash:7].[ext]') + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + options: { + // limit: 10000, + name: utils.assetsPath('fonts/[name].[hash:7].[ext]') + } + } + ] + } +} diff --git a/assets/build/webpack.dev.conf.js b/assets/build/webpack.dev.conf.js index dd75ac4e2a..3276b7ead6 100644 --- a/assets/build/webpack.dev.conf.js +++ b/assets/build/webpack.dev.conf.js @@ -1,81 +1,81 @@ -var fs = require('fs') -var path = require('path') -var utils = require('./utils') -var webpack = require('webpack') -var config = require('./config') -var merge = require('webpack-merge') -var baseWebpackConfig = require('./webpack.base.conf') -var HtmlWebpackPlugin = require('html-webpack-plugin') -var ExtractTextPlugin = require('extract-text-webpack-plugin') -var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') -var CopyWebpackPlugin = require('copy-webpack-plugin') - -module.exports = merge(baseWebpackConfig, { - watch: true, - module: { - rules: utils.styleLoaders({ - sourceMap: config.dev.produceSourceMap, - extract: true - }) - }, - devtool: '#cheap-module-eval-source-map', - output: { - path: config.assetsRoot, - filename: utils.assetsPath('js/[name].[chunkhash].js'), - chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') - }, - plugins: [ - new webpack.NoEmitOnErrorsPlugin(), - new FriendlyErrorsPlugin(), - new webpack.DefinePlugin({ - 'process.env': config.dev.env - }), - // extract css into its own file - new ExtractTextPlugin({ - filename: utils.assetsPath('css/[name].[contenthash].css') - }), - // generate dist index.html with correct asset hash for caching. - // you can customize output by editing /index.html - // see https://github.com/ampedandwired/html-webpack-plugin - new HtmlWebpackPlugin({ - filename: config.index, - template: 'assets/index.html', - inject: true, - // necessary to consistently work with multiple chunks via CommonsChunkPlugin - chunksSortMode: 'dependency', - serviceWorkerLoader: `` - }), - // split vendor js into its own file - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - minChunks: function (module, count) { - // any required modules inside node_modules are extracted to vendor - return ( - module.resource && - /\.js$/.test(module.resource) && - module.resource.indexOf( - path.join(__dirname, '../../node_modules') - ) === 0 - ) - } - }), - // extract webpack runtime and module manifest to its own file in order to - // prevent vendor hash from being updated whenever app bundle is updated - new webpack.optimize.CommonsChunkPlugin({ - name: 'manifest', - chunks: ['vendor'] - }), - new CopyWebpackPlugin([ - { - from: path.resolve(__dirname, '../static'), - to: config.assetsSubDirectory, - ignore: ['.*'] - }, - { - from: path.resolve(__dirname, '../../node_modules/codemirror/mode/*/*'), - to: path.join(config.assetsSubDirectory, 'js/codemirror/mode/[name]/[name].js') - } - ]) - ] -}) +var fs = require('fs') +var path = require('path') +var utils = require('./utils') +var webpack = require('webpack') +var config = require('./config') +var merge = require('webpack-merge') +var baseWebpackConfig = require('./webpack.base.conf') +var HtmlWebpackPlugin = require('html-webpack-plugin') +var ExtractTextPlugin = require('extract-text-webpack-plugin') +var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') +var CopyWebpackPlugin = require('copy-webpack-plugin') + +module.exports = merge(baseWebpackConfig, { + watch: true, + module: { + rules: utils.styleLoaders({ + sourceMap: config.dev.produceSourceMap, + extract: true + }) + }, + devtool: '#cheap-module-eval-source-map', + output: { + path: config.assetsRoot, + filename: utils.assetsPath('js/[name].[chunkhash].js'), + chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') + }, + plugins: [ + new webpack.NoEmitOnErrorsPlugin(), + new FriendlyErrorsPlugin(), + new webpack.DefinePlugin({ + 'process.env': config.dev.env + }), + // extract css into its own file + new ExtractTextPlugin({ + filename: utils.assetsPath('css/[name].[contenthash].css') + }), + // generate dist index.html with correct asset hash for caching. + // you can customize output by editing /index.html + // see https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: config.index, + template: 'assets/index.html', + inject: true, + // necessary to consistently work with multiple chunks via CommonsChunkPlugin + chunksSortMode: 'dependency', + serviceWorkerLoader: `` + }), + // split vendor js into its own file + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: function (module, count) { + // any required modules inside node_modules are extracted to vendor + return ( + module.resource && + /\.js$/.test(module.resource) && + module.resource.indexOf( + path.join(__dirname, '../../node_modules') + ) === 0 + ) + } + }), + // extract webpack runtime and module manifest to its own file in order to + // prevent vendor hash from being updated whenever app bundle is updated + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest', + chunks: ['vendor'] + }), + new CopyWebpackPlugin([ + { + from: path.resolve(__dirname, '../static'), + to: config.assetsSubDirectory, + ignore: ['.*'] + }, + { + from: path.resolve(__dirname, '../../node_modules/codemirror/mode/*/*'), + to: path.join(config.assetsSubDirectory, 'js/codemirror/mode/[name]/[name].js') + } + ]) + ] +}) diff --git a/assets/src/assets/logo.svg b/assets/src/assets/logo.svg index 62e6798ac0..4284f68481 100644 --- a/assets/src/assets/logo.svg +++ b/assets/src/assets/logo.svg @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/assets/src/components/Languages.vue b/assets/src/components/Languages.vue index 41301cca33..46911c8c97 100644 --- a/assets/src/components/Languages.vue +++ b/assets/src/components/Languages.vue @@ -1,22 +1,22 @@ - - - + + + diff --git a/assets/src/components/Search.vue b/assets/src/components/Search.vue index b5b434f33e..0f7195880a 100644 --- a/assets/src/components/Search.vue +++ b/assets/src/components/Search.vue @@ -1,265 +1,265 @@ - - - - + + + diff --git a/assets/src/components/buttons/Copy.vue b/assets/src/components/buttons/Copy.vue index 164cf3471b..810061bd93 100644 --- a/assets/src/components/buttons/Copy.vue +++ b/assets/src/components/buttons/Copy.vue @@ -1,17 +1,17 @@ - - - + + + diff --git a/assets/src/components/buttons/Delete.vue b/assets/src/components/buttons/Delete.vue index bb3c0fdff6..9879087d0a 100644 --- a/assets/src/components/buttons/Delete.vue +++ b/assets/src/components/buttons/Delete.vue @@ -1,17 +1,17 @@ - - - + + + diff --git a/assets/src/components/buttons/Download.vue b/assets/src/components/buttons/Download.vue index bcef980b2a..9994309447 100644 --- a/assets/src/components/buttons/Download.vue +++ b/assets/src/components/buttons/Download.vue @@ -1,39 +1,39 @@ - - - + + + diff --git a/assets/src/components/buttons/Info.vue b/assets/src/components/buttons/Info.vue index 164e59c948..d5a380907d 100644 --- a/assets/src/components/buttons/Info.vue +++ b/assets/src/components/buttons/Info.vue @@ -1,17 +1,17 @@ - - - + + + diff --git a/assets/src/components/buttons/Move.vue b/assets/src/components/buttons/Move.vue index 5d9250f413..4b348e15b6 100644 --- a/assets/src/components/buttons/Move.vue +++ b/assets/src/components/buttons/Move.vue @@ -1,17 +1,17 @@ - - - + + + diff --git a/assets/src/components/buttons/Rename.vue b/assets/src/components/buttons/Rename.vue index a2fd2cbf9f..73b61d33e6 100644 --- a/assets/src/components/buttons/Rename.vue +++ b/assets/src/components/buttons/Rename.vue @@ -1,17 +1,17 @@ - - - + + + diff --git a/assets/src/components/buttons/Schedule.vue b/assets/src/components/buttons/Schedule.vue index 0484d6bd05..1927845d00 100644 --- a/assets/src/components/buttons/Schedule.vue +++ b/assets/src/components/buttons/Schedule.vue @@ -1,21 +1,21 @@ - - - + + + diff --git a/assets/src/components/buttons/Upload.vue b/assets/src/components/buttons/Upload.vue index f5d738ad7a..82e8aab9d2 100644 --- a/assets/src/components/buttons/Upload.vue +++ b/assets/src/components/buttons/Upload.vue @@ -1,17 +1,17 @@ - - - + + + diff --git a/assets/src/components/files/ListingItem.vue b/assets/src/components/files/ListingItem.vue index 8b8a64965b..3217ee20de 100644 --- a/assets/src/components/files/ListingItem.vue +++ b/assets/src/components/files/ListingItem.vue @@ -119,9 +119,21 @@ export default { } if (event.shiftKey && this.selected.length === 1) { - let fi = (this.index > this.selected[0]) ? this.selected[0] : this.index - let la = (this.index > this.selected[0]) ? this.index : this.selected[0] - for (; fi <= la; fi++) this.addSelected(fi) + let fi = 0 + let la = 0 + + if (this.index > this.selected[0]) { + fi = this.selected[0] + 1 + la = this.index + } else { + fi = this.index + la = this.selected[0] - 1 + } + + for (; fi <= la; fi++) { + this.addSelected(fi) + } + return } diff --git a/assets/src/components/prompts/Rename.vue b/assets/src/components/prompts/Rename.vue index 3c65b99b89..c5af3afc22 100644 --- a/assets/src/components/prompts/Rename.vue +++ b/assets/src/components/prompts/Rename.vue @@ -1,84 +1,84 @@ - - - + + + diff --git a/assets/src/css/editor.css b/assets/src/css/editor.css index 971bf4236d..d96db5aa36 100644 --- a/assets/src/css/editor.css +++ b/assets/src/css/editor.css @@ -1,184 +1,184 @@ -@import "~codemirror/lib/codemirror.css"; -@import "~codemirror/theme/ttcn.css"; -#editor { - max-width: 800px; - margin: 0 auto; -} - -#editor .CodeMirror { - box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; - margin: 2em 0; - border-radius: .5em; -} - -#editor h2 { - color: rgba(0, 0, 0, 0.3); - font-weight: 500; -} - -.CodeMirror { - height: auto; -} - -.markdown .CodeMirror { - padding: .75em; -} - -.cm-s-markdown .CodeMirror-gutter { - border-right: 1px solid #eff3f5; - padding-right: 5px; - margin-right: 15px; - min-width: 2.5em; - padding-bottom: 30px; -} - -.cm-s-markdown .CodeMirror-cursor { - border-right: 2px solid #667880; -} - -.cm-s-markdown .CodeMirror-lines { - margin: 0; -} - -.cm-s-markdown { - color: #3D494E; -} - -.cm-s-markdown span.cm-header { - color: #3D494E; - font-weight: bold; -} - -.cm-s-markdown span.cm-variable-2 { - color: #3D494E; -} - -.cm-s-markdown span.cm-meta { - color: #516066; -} - -.cm-s-markdown span.cm-hr { - color: #516066; -} - -.cm-s-markdown span.cm-comment { - color: #868f93; -} - -.cm-s-markdown span.cm-qualifier { - color: #868f93; -} - -.cm-s-markdown span.cm-number { - color: #197987; -} - -.cm-s-markdown span.cm-variable { - color: #197987; -} - -.cm-s-markdown span.cm-builtin { - color: #197987; -} - -.cm-s-markdown span.cm-link { - color: #197987; - text-decoration: underline; -} - -.cm-s-markdown span.cm-tag { - color: #197987; -} - -.cm-s-markdown span.cm-string { - color: #48abb9; -} - -.cm-s-markdown span.cm-string-2 { - color: #48abb9; -} - -.cm-s-markdown span.cm-quote { - color: #48abb9; -} - -.cm-s-markdown span.cm-atom { - color: #48abb9; -} - -.cm-s-markdown span.cm-property { - color: #82a367; -} - -.cm-s-markdown span.cm-operator { - color: #82a367; -} - -.cm-s-markdown span.cm-variable-3 { - color: #82a367; -} - -.cm-s-markdown span.cm-attribute { - color: #90bb74; -} - -.cm-s-markdown span.cm-def { - color: #90bb74; -} - -.cm-s-markdown span.cm-keyword { - color: #ec6c45; -} - -.cm-s-markdown span.cm-bracket { - color: #ec6c45; -} - -.cm-s-markdown span.cm-error { - color: #e45346; -} - -.cm-s-markdown span.cm-em { - font-style: italic; -} - -.cm-s-markdown span.cm-strong { - font-weight: bold; -} - -.cm-s-markdown .cm-header-1 { - font-size: 200%; - line-height: 200%; -} - -.cm-s-markdown .cm-header-2 { - font-size: 160%; - line-height: 160%; -} - -.cm-s-markdown .cm-header-3 { - font-size: 125%; - line-height: 125%; -} - -.cm-s-markdown .cm-header-4 { - font-size: 110%; - line-height: 110%; -} - -.cm-s-markdown .cm-comment { - background: rgba(0, 0, 0, .05); - border-radius: 2px; -} - -.cm-s-markdown .cm-link { - color: #7f8c8d; -} - -.cm-s-markdown .cm-url { - color: #aab2b3; -} - -.cm-s-markdown .cm-strikethrough { - text-decoration: line-through; -} +@import "~codemirror/lib/codemirror.css"; +@import "~codemirror/theme/ttcn.css"; +#editor { + max-width: 800px; + margin: 0 auto; +} + +#editor .CodeMirror { + box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; + margin: 2em 0; + border-radius: .5em; +} + +#editor h2 { + color: rgba(0, 0, 0, 0.3); + font-weight: 500; +} + +.CodeMirror { + height: auto; +} + +.markdown .CodeMirror { + padding: .75em; +} + +.cm-s-markdown .CodeMirror-gutter { + border-right: 1px solid #eff3f5; + padding-right: 5px; + margin-right: 15px; + min-width: 2.5em; + padding-bottom: 30px; +} + +.cm-s-markdown .CodeMirror-cursor { + border-right: 2px solid #667880; +} + +.cm-s-markdown .CodeMirror-lines { + margin: 0; +} + +.cm-s-markdown { + color: #3D494E; +} + +.cm-s-markdown span.cm-header { + color: #3D494E; + font-weight: bold; +} + +.cm-s-markdown span.cm-variable-2 { + color: #3D494E; +} + +.cm-s-markdown span.cm-meta { + color: #516066; +} + +.cm-s-markdown span.cm-hr { + color: #516066; +} + +.cm-s-markdown span.cm-comment { + color: #868f93; +} + +.cm-s-markdown span.cm-qualifier { + color: #868f93; +} + +.cm-s-markdown span.cm-number { + color: #197987; +} + +.cm-s-markdown span.cm-variable { + color: #197987; +} + +.cm-s-markdown span.cm-builtin { + color: #197987; +} + +.cm-s-markdown span.cm-link { + color: #197987; + text-decoration: underline; +} + +.cm-s-markdown span.cm-tag { + color: #197987; +} + +.cm-s-markdown span.cm-string { + color: #48abb9; +} + +.cm-s-markdown span.cm-string-2 { + color: #48abb9; +} + +.cm-s-markdown span.cm-quote { + color: #48abb9; +} + +.cm-s-markdown span.cm-atom { + color: #48abb9; +} + +.cm-s-markdown span.cm-property { + color: #82a367; +} + +.cm-s-markdown span.cm-operator { + color: #82a367; +} + +.cm-s-markdown span.cm-variable-3 { + color: #82a367; +} + +.cm-s-markdown span.cm-attribute { + color: #90bb74; +} + +.cm-s-markdown span.cm-def { + color: #90bb74; +} + +.cm-s-markdown span.cm-keyword { + color: #ec6c45; +} + +.cm-s-markdown span.cm-bracket { + color: #ec6c45; +} + +.cm-s-markdown span.cm-error { + color: #e45346; +} + +.cm-s-markdown span.cm-em { + font-style: italic; +} + +.cm-s-markdown span.cm-strong { + font-weight: bold; +} + +.cm-s-markdown .cm-header-1 { + font-size: 200%; + line-height: 200%; +} + +.cm-s-markdown .cm-header-2 { + font-size: 160%; + line-height: 160%; +} + +.cm-s-markdown .cm-header-3 { + font-size: 125%; + line-height: 125%; +} + +.cm-s-markdown .cm-header-4 { + font-size: 110%; + line-height: 110%; +} + +.cm-s-markdown .cm-comment { + background: rgba(0, 0, 0, .05); + border-radius: 2px; +} + +.cm-s-markdown .cm-link { + color: #7f8c8d; +} + +.cm-s-markdown .cm-url { + color: #aab2b3; +} + +.cm-s-markdown .cm-strikethrough { + text-decoration: line-through; +} diff --git a/assets/src/css/fonts.css b/assets/src/css/fonts.css index f355f0e895..f0400c315a 100644 --- a/assets/src/css/fonts.css +++ b/assets/src/css/fonts.css @@ -1,137 +1,137 @@ -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format('woff2'); - unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic.woff2) format('woff2'); - unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek-ext.woff2) format('woff2'); - unicode-range: U+1F00-1FFF; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek.woff2) format('woff2'); - unicode-range: U+0370-03FF; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-vietnamese.woff2) format('woff2'); - unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin-ext.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format('woff2'); - unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic.woff2) format('woff2'); - unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek-ext.woff2) format('woff2'); - unicode-range: U+1F00-1FFF; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek.woff2) format('woff2'); - unicode-range: U+0370-03FF; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-vietnamese.woff2) format('woff2'); - unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin-ext.woff2) format('woff2'); - unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; -} - -@font-face { - font-family: 'Material Icons'; - font-style: normal; - font-weight: 400; - src: local('Material Icons'), local('MaterialIcons-Regular'), url(../assets/fonts/material/icons.woff2) format('woff2'); -} - -.prompt .file-list ul li:before, -.material-icons { - font-family: 'Material Icons'; - font-weight: normal; - font-style: normal; - font-size: 24px; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - -moz-osx-font-smoothing: grayscale; - font-feature-settings: 'liga'; -} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek-ext.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format('woff2'); + unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek-ext.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek.woff2) format('woff2'); + unicode-range: U+0370-03FF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; +} + +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), local('MaterialIcons-Regular'), url(../assets/fonts/material/icons.woff2) format('woff2'); +} + +.prompt .file-list ul li:before, +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: 'liga'; +} diff --git a/assets/src/css/mobile.css b/assets/src/css/mobile.css index 05d4d46b77..79d21b905a 100644 --- a/assets/src/css/mobile.css +++ b/assets/src/css/mobile.css @@ -1,113 +1,113 @@ -@media (max-width: 1024px) { - nav { - width: 10em - } -} - -@media (max-width: 1024px) { - #listing.list .item.header, - main { - width: calc(100% - 13em) - } -} - -@media (max-width: 736px) { - #more { - display: inherit - } - header .overlay { - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.1); - } - #dropdown { - position: fixed; - top: 1em; - right: 1em; - display: block; - background-color: #fff; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); - transform: scale(0); - transition: .1s ease-in-out transform; - transform-origin: top right; - z-index: 99999; - } - #dropdown > div { - display: block; - } - #dropdown.active { - transform: scale(1); - } - #dropdown .action { - display: flex; - align-items: center; - border-radius: 0; - width: 100%; - } - #dropdown .action span:not(.counter) { - display: inline-block; - padding: .4em; - } - #dropdown .counter { - left: 2.25em; - } - #file-selection { - position: fixed; - bottom: 1em; - left: 50%; - transform: translateX(-50%); - display: flex; - align-items: center; - background: #fff; - box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; - width: 95%; - max-width: 20em; - } - #file-selection .action { - border-radius: 50%; - width: auto; - } - #file-selection > span { - display: inline-block; - margin-left: 1em; - color: #6f6f6f; - margin-right: auto; - } - nav { - top: 0; - z-index: 99999; - background: #fff; - height: 100%; - width: 16em; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); - transition: .1s ease left; - left: -17em; - } - nav.active { - left: 0; - } - header .search-button, - header>div:first-child>.action { - display: inherit; - } - header img { - display: none; - } - #listing { - margin-bottom: 5em; - } - #listing.list .item.header, - main { - width: calc(100% - 2em); - } - main { - margin: 0 1em; - width: calc(100% - 2em); - } - #search { - display: none; - } - #search.active { - display: block; - } -} +@media (max-width: 1024px) { + nav { + width: 10em + } +} + +@media (max-width: 1024px) { + #listing.list .item.header, + main { + width: calc(100% - 13em) + } +} + +@media (max-width: 736px) { + #more { + display: inherit + } + header .overlay { + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.1); + } + #dropdown { + position: fixed; + top: 1em; + right: 1em; + display: block; + background-color: #fff; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + transform: scale(0); + transition: .1s ease-in-out transform; + transform-origin: top right; + z-index: 99999; + } + #dropdown > div { + display: block; + } + #dropdown.active { + transform: scale(1); + } + #dropdown .action { + display: flex; + align-items: center; + border-radius: 0; + width: 100%; + } + #dropdown .action span:not(.counter) { + display: inline-block; + padding: .4em; + } + #dropdown .counter { + left: 2.25em; + } + #file-selection { + position: fixed; + bottom: 1em; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + background: #fff; + box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; + width: 95%; + max-width: 20em; + } + #file-selection .action { + border-radius: 50%; + width: auto; + } + #file-selection > span { + display: inline-block; + margin-left: 1em; + color: #6f6f6f; + margin-right: auto; + } + nav { + top: 0; + z-index: 99999; + background: #fff; + height: 100%; + width: 16em; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); + transition: .1s ease left; + left: -17em; + } + nav.active { + left: 0; + } + header .search-button, + header>div:first-child>.action { + display: inherit; + } + header img { + display: none; + } + #listing { + margin-bottom: 5em; + } + #listing.list .item.header, + main { + width: calc(100% - 2em); + } + main { + margin: 0 1em; + width: calc(100% - 2em); + } + #search { + display: none; + } + #search.active { + display: block; + } +} diff --git a/assets/src/i18n/ja.yaml b/assets/src/i18n/ja.yaml index a961e42242..9be68ea489 100644 --- a/assets/src/i18n/ja.yaml +++ b/assets/src/i18n/ja.yaml @@ -1,200 +1,200 @@ -permanent: 永久 -buttons: - cancel: キャンセル - close: 閉じる - copy: コピー - copyFile: ファイルをコピー - copyToClipboard: クリップボードにコピー - create: 作成 - delete: 削除 - download: ダウンロード - info: 情報 - more: More - move: 移動 - moveFile: ファイルを移動 - new: 新規 - next: 次 - ok: OK - replace: 置き換える - previous: 前 - rename: 名前を変更 - reportIssue: 問題を報告 - save: 保存 - search: 検索 - select: 選択 - share: シェア - publish: 発表 - selectMultiple: 複数選択 - schedule: スケジュール - switchView: 表示を切り替わる - toggleSidebar: サイドバーを表示する - update: 更新 - upload: アップロード - permalink: 固定リンク -success: - linkCopied: リンクがコピーされました! -errors: - forbidden: アクセスが拒否されました。 - internal: 内部エラーが発生しました。 - notFound: リソースが見つからなりませんでした。 -files: - folders: フォルダ - files: ファイル - body: 本文 - clear: クリアー - closePreview: プレビューを閉じる - home: ホーム - lastModified: 最終変更 - loading: ローディング... - lonely: ここには何もない... - metadata: メタデータ - multipleSelectionEnabled: 複数選択有効 - name: 名前 - size: サイズ - sortByName: 名前によるソート - sortBySize: サイズによるソート - sortByLastModified: 最終変更日付によるソート -help: - click: ファイルやディレクトリを選択 - ctrl: - click: 複数のファイルやディレクトリを選択 - f: 検索を有効にする - s: ファイルを保存またはカレントディレクトリをダウンロード - del: 選択した項目を削除 - doubleClick: ファイルやディレクトリをオープン - esc: 選択をクリアーまたはプロンプトを閉じる - f1: このヘルプを表示 - f2: ファイルの名前を変更 - help: ヘルプ -login: - password: パスワード - submit: ログイン - username: ユーザ名 - wrongCredentials: ユーザ名またはパスワードが間違っています。 -prompts: - copy: コピー - copyMessage: コピーの目標ディレクトリを選択してください: - currentlyNavigating: 現在閲覧しているディレクトリ: - deleteMessageMultiple: '{count} つのファイルを本当に削除してよろしいですか。' - deleteMessageSingle: このファイル/フォルダを本当に削除してよろしいですか。 - deleteTitle: ファイルを削除 - displayName: 名前: - download: ファイルをダウンロード - downloadMessage: 圧縮形式を選択してください。 - error: あるエラーが発生しました。 - fileInfo: ファイル情報 - filesSelected: '{count} つのファイルは選択されました。' - lastModified: 最終変更 - move: 移動 - moveMessage: 移動の目標ディレクトリを選択してください: - newDir: 新しいディレクトリを作成 - newDirMessage: 新しいディレクトリの名前を入力してください。 - newFile: 新しいファイルを作成 - newFileMessage: 新しいファイルの名前を入力してください。 - numberDirs: ディレクトリ個数 - numberFiles: ファイル個数 - replace: 置き換える - replaceMessage: > - アップロードするファイルの中でかち合う名前が一つあります。 - 既存のファイルを置き換えりませんか。 - rename: 名前を変更 - renameMessage: 名前を変更しようファイルは: - show: 表示 - size: サイズ - schedule: スケジュール - scheduleMessage: このポストの発表日付をスケジュールしてください。 - newArchetype: ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。 -settings: - admin: 管理者 - administrator: 管理者 - allowCommands: コマンドの実行 - allowEdit: ファイルやディレクトリの編集、名前変更と削除 - allowNew: ファイルとディレクトリの作成 - allowPublish: ポストとぺーじの発表 - avoidChanges: "(変更を避けるために空白にしてください)" - changePassword: パスワードを変更 - commands: コマンド - commandsHelp: "\ - ここで、名前付きイベントに実行するコマンドを設定することができます。\ - 一行にコマンド一つを入力してください。\ - イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\ - 環境変数 FILE はファイルのパスに割り当てられます。" - commandsUpdated: コマンドは更新されました! - customStylesheet: カスタムスタイルシ ート - examples: 例 - globalSettings: グローバル設定 - language: 言語 - lockPassword: 新しいパスワードを変更に禁止 - newPassword: 新しいパスワード - newPasswordConfirm: 新しいパスワードを確認します - newUser: 新しいユーザー - password: パスワード - passwordUpdated: パスワードは更新されました! - permissions: 権限 - permissionsHelp: "\ - あなたはユーザーを管理者に設定し、または権限を個々に設定しできます。\ - \"管理者\"を選択する場合、その他のすべての選択肢は自動的に設定されます。\ - ユーザーの管理は管理者の権限として保留されました。" - profileSettings: プロファイル設定 - ruleExample1: "\ - 各フォルダに名前はドットで始まるファイル(例えば、.git、.gitignore)\ - へのアクセスを制限します。" - ruleExample2: 範囲のルートパスに名前は Caddyfile のファイルへのアクセスを制限します。 - rules: 規則 - rulesHelp1: "\ - ここに、あなたはこのユーザーの許可または拒否規則を設定できます。\ - ブロックされたファイルはリストに表示されません、それではアクセスも制限されます。\ - 正規表現(regex)のサポートと範囲に相対のパスが提供されています。" - rulesHelp2: "\ - 一行に規則一つを入力してください、\ - その間に規則はキーワード {0} や {1} で始める必要があります。\ - そして正規表現を使う場合、{2} と入力し、表現やパスを入力してください。" - scope: 範囲 - settingsUpdated: 設定は更新されました! - user: ユーザー - userCommands: ユーザーのコマンド - userCommandsHelp: "\ - 空白区切りの有効のコマンドのリストを指定してください。\ - 例:" - userCreated: ユーザーは作成されました! - userDeleted: ユーザーは削除されました! - userManagement: ユーザー管理 - username: ユーザー名 - users: ユーザー - userUpdated: ユーザーは更新されました! -sidebar: - help: ヘルプ - logout: ログアウト - myFiles: 私のファイル - newFile: 新しいファイルを作成 - newFolder: 新しいフォルダを作成 - settings: 設定 - siteSettings: サイト設定 - hugoNew: Hugo New - preview: プレビュー -search: - images: 画像 - music: 音楽 - pdf: PDF - pressToExecute: Enter を押して実行します。 - pressToSearch: Enter を押して検索します。 - search: 検索... - searchOrCommand: コマンドを検索または実行します。 - searchOrSupportedCommand: サポートしているコマンドを検索または実行します: - type: キーワードを入力し、Enter を押して検索します。 - types: 種類 - video: ビデオ - writeToSearch: ここにキーワードを入力してください -languages: - en: English - fr: Français - pt: Português - ja: 日本語 - zhCN: 中文 (简体) - zhTW: 中文 (繁體) -time: - unit: 時間単位 - seconds: 秒 - minutes: 分 - hours: 時間 - days: 日 +permanent: 永久 +buttons: + cancel: キャンセル + close: 閉じる + copy: コピー + copyFile: ファイルをコピー + copyToClipboard: クリップボードにコピー + create: 作成 + delete: 削除 + download: ダウンロード + info: 情報 + more: More + move: 移動 + moveFile: ファイルを移動 + new: 新規 + next: 次 + ok: OK + replace: 置き換える + previous: 前 + rename: 名前を変更 + reportIssue: 問題を報告 + save: 保存 + search: 検索 + select: 選択 + share: シェア + publish: 発表 + selectMultiple: 複数選択 + schedule: スケジュール + switchView: 表示を切り替わる + toggleSidebar: サイドバーを表示する + update: 更新 + upload: アップロード + permalink: 固定リンク +success: + linkCopied: リンクがコピーされました! +errors: + forbidden: アクセスが拒否されました。 + internal: 内部エラーが発生しました。 + notFound: リソースが見つからなりませんでした。 +files: + folders: フォルダ + files: ファイル + body: 本文 + clear: クリアー + closePreview: プレビューを閉じる + home: ホーム + lastModified: 最終変更 + loading: ローディング... + lonely: ここには何もない... + metadata: メタデータ + multipleSelectionEnabled: 複数選択有効 + name: 名前 + size: サイズ + sortByName: 名前によるソート + sortBySize: サイズによるソート + sortByLastModified: 最終変更日付によるソート +help: + click: ファイルやディレクトリを選択 + ctrl: + click: 複数のファイルやディレクトリを選択 + f: 検索を有効にする + s: ファイルを保存またはカレントディレクトリをダウンロード + del: 選択した項目を削除 + doubleClick: ファイルやディレクトリをオープン + esc: 選択をクリアーまたはプロンプトを閉じる + f1: このヘルプを表示 + f2: ファイルの名前を変更 + help: ヘルプ +login: + password: パスワード + submit: ログイン + username: ユーザ名 + wrongCredentials: ユーザ名またはパスワードが間違っています。 +prompts: + copy: コピー + copyMessage: コピーの目標ディレクトリを選択してください: + currentlyNavigating: 現在閲覧しているディレクトリ: + deleteMessageMultiple: '{count} つのファイルを本当に削除してよろしいですか。' + deleteMessageSingle: このファイル/フォルダを本当に削除してよろしいですか。 + deleteTitle: ファイルを削除 + displayName: 名前: + download: ファイルをダウンロード + downloadMessage: 圧縮形式を選択してください。 + error: あるエラーが発生しました。 + fileInfo: ファイル情報 + filesSelected: '{count} つのファイルは選択されました。' + lastModified: 最終変更 + move: 移動 + moveMessage: 移動の目標ディレクトリを選択してください: + newDir: 新しいディレクトリを作成 + newDirMessage: 新しいディレクトリの名前を入力してください。 + newFile: 新しいファイルを作成 + newFileMessage: 新しいファイルの名前を入力してください。 + numberDirs: ディレクトリ個数 + numberFiles: ファイル個数 + replace: 置き換える + replaceMessage: > + アップロードするファイルの中でかち合う名前が一つあります。 + 既存のファイルを置き換えりませんか。 + rename: 名前を変更 + renameMessage: 名前を変更しようファイルは: + show: 表示 + size: サイズ + schedule: スケジュール + scheduleMessage: このポストの発表日付をスケジュールしてください。 + newArchetype: ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。 +settings: + admin: 管理者 + administrator: 管理者 + allowCommands: コマンドの実行 + allowEdit: ファイルやディレクトリの編集、名前変更と削除 + allowNew: ファイルとディレクトリの作成 + allowPublish: ポストとぺーじの発表 + avoidChanges: "(変更を避けるために空白にしてください)" + changePassword: パスワードを変更 + commands: コマンド + commandsHelp: "\ + ここで、名前付きイベントに実行するコマンドを設定することができます。\ + 一行にコマンド一つを入力してください。\ + イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\ + 環境変数 FILE はファイルのパスに割り当てられます。" + commandsUpdated: コマンドは更新されました! + customStylesheet: カスタムスタイルシ ート + examples: 例 + globalSettings: グローバル設定 + language: 言語 + lockPassword: 新しいパスワードを変更に禁止 + newPassword: 新しいパスワード + newPasswordConfirm: 新しいパスワードを確認します + newUser: 新しいユーザー + password: パスワード + passwordUpdated: パスワードは更新されました! + permissions: 権限 + permissionsHelp: "\ + あなたはユーザーを管理者に設定し、または権限を個々に設定しできます。\ + \"管理者\"を選択する場合、その他のすべての選択肢は自動的に設定されます。\ + ユーザーの管理は管理者の権限として保留されました。" + profileSettings: プロファイル設定 + ruleExample1: "\ + 各フォルダに名前はドットで始まるファイル(例えば、.git、.gitignore)\ + へのアクセスを制限します。" + ruleExample2: 範囲のルートパスに名前は Caddyfile のファイルへのアクセスを制限します。 + rules: 規則 + rulesHelp1: "\ + ここに、あなたはこのユーザーの許可または拒否規則を設定できます。\ + ブロックされたファイルはリストに表示されません、それではアクセスも制限されます。\ + 正規表現(regex)のサポートと範囲に相対のパスが提供されています。" + rulesHelp2: "\ + 一行に規則一つを入力してください、\ + その間に規則はキーワード {0} や {1} で始める必要があります。\ + そして正規表現を使う場合、{2} と入力し、表現やパスを入力してください。" + scope: 範囲 + settingsUpdated: 設定は更新されました! + user: ユーザー + userCommands: ユーザーのコマンド + userCommandsHelp: "\ + 空白区切りの有効のコマンドのリストを指定してください。\ + 例:" + userCreated: ユーザーは作成されました! + userDeleted: ユーザーは削除されました! + userManagement: ユーザー管理 + username: ユーザー名 + users: ユーザー + userUpdated: ユーザーは更新されました! +sidebar: + help: ヘルプ + logout: ログアウト + myFiles: 私のファイル + newFile: 新しいファイルを作成 + newFolder: 新しいフォルダを作成 + settings: 設定 + siteSettings: サイト設定 + hugoNew: Hugo New + preview: プレビュー +search: + images: 画像 + music: 音楽 + pdf: PDF + pressToExecute: Enter を押して実行します。 + pressToSearch: Enter を押して検索します。 + search: 検索... + searchOrCommand: コマンドを検索または実行します。 + searchOrSupportedCommand: サポートしているコマンドを検索または実行します: + type: キーワードを入力し、Enter を押して検索します。 + types: 種類 + video: ビデオ + writeToSearch: ここにキーワードを入力してください +languages: + en: English + fr: Français + pt: Português + ja: 日本語 + zhCN: 中文 (简体) + zhTW: 中文 (繁體) +time: + unit: 時間単位 + seconds: 秒 + minutes: 分 + hours: 時間 + days: 日 diff --git a/assets/src/i18n/zh-cn.yaml b/assets/src/i18n/zh-cn.yaml index 9556665391..05f0191b0d 100644 --- a/assets/src/i18n/zh-cn.yaml +++ b/assets/src/i18n/zh-cn.yaml @@ -1,198 +1,198 @@ -permanent: 永久 -buttons: - cancel: 取消 - close: 关闭 - copy: 复制 - copyFile: 复制文件 - copyToClipboard: 复制到剪贴板 - create: 创建 - delete: 删除 - download: 下载 - info: 信息 - more: 更多 - move: 移动 - moveFile: 移动文件 - new: 新 - next: 下一个 - ok: 确定 - replace: 替换 - previous: 上一个 - rename: 重命名 - reportIssue: 报告问题 - save: 保存 - search: 搜索 - select: 选择 - share: 分享 - publish: 发布 - selectMultiple: 选择多个 - schedule: 计划 - switchView: 切换显示方式 - toggleSidebar: 切换侧边栏 - update: 更新 - upload: 上传 - permalink: 获取永久链接 -success: - linkCopied: 链接已复制! -errors: - forbidden: 你被禁止访问。 - internal: 内部出现麻烦了。 - notFound: 找不到文件。 -files: - folders: 文件夹 - files: 文件 - body: Body - clear: 清空 - closePreview: 关闭预览 - home: 主页 - lastModified: 最后修改 - loading: 加载中... - lonely: 这里没有任何文件... - metadata: 元数据 - multipleSelectionEnabled: 多选模式已开启 - name: 名称 - size: 大小 - sortByName: 按名称排序 - sortBySize: 按大小排序 - sortByLastModified: 按最后修改时间排序 -help: - click: 选择文件或目录 - ctrl: - click: 选择多个文件或目录 - f: 打开搜索框 - s: 保存文件或下载当前文件夹 - del: 删除所选的文件/文件夹 - doubleClick: 打开文件/文件夹 - esc: 清除已选项或关闭提示信息 - f1: 显示该帮助信息 - f2: 重命名文件/文件夹 - help: 帮助 -login: - password: 密码 - submit: 登录 - username: 用户名 - wrongCredentials: 用户名或密码错误 -prompts: - copy: 复制 - copyMessage: 请选择欲复制至的目录: - currentlyNavigating: 当前目录: - deleteMessageMultiple: 你确定要删除这 {count} 个文件吗? - deleteMessageSingle: 你确定要删除这个文件/文件夹吗? - deleteTitle: 删除文件 - displayName: 名称: - download: 下载文件 - downloadMessage: 请选择要下载的压缩格式。 - error: 出了一点问题... - fileInfo: 文件信息 - filesSelected: 已选择 {count} 个文件。 - lastModified: 最后修改 - move: 移动 - moveMessage: 请选择欲移动至的目录: - newDir: 新建目录 - newDirMessage: 请输入新目录的名称。 - newFile: 新建文件 - newFileMessage: 请输入新文件的名称。 - numberDirs: 目录数 - numberFiles: 文件数 - replace: 替换 - replaceMessage: "\ - 您尝试上传的文件中有一个与现有文件的名称存在冲突。\ - 是否替换现有的同名文件?" - rename: 重命名 - renameMessage: 请输入新名称,旧名称为: - show: 揭示 - size: 大小 - schedule: 计划 - scheduleMessage: 请选择发布这篇帖子的日期。 - newArchetype: 创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。 -settings: - admin: 管理员 - administrator: 管理员 - allowCommands: 执行命令(Linux 代码) - allowEdit: 编辑、重命名或删除文件/目录 - allowNew: 创建新文件和目录 - allowPublish: 发布新的帖子与页面 - avoidChanges: '(留空以避免更改)' - changePassword: 更改密码 - commands: 命令(linux 代码) - commandsHelp: "\ - 在这里,您可以设置在指定事件下执行的命令,一行一条。\ - 若事件与文件相关,如“在保存文件前”,\ - 则文件的路径会被赋值给环境变量 \"FILE\"。" - commandsUpdated: 命令已更新! - customStylesheet: 自定义样式表 - examples: 例子 - globalSettings: 全局设置 - language: 语言 - lockPassword: 禁止用户修改密码 - newPassword: 您的新密码 - newPasswordConfirm: 重输一遍新密码 - newUser: 新建用户 - password: 密码 - passwordUpdated: 密码已更新! - permissions: 权限 - permissionsHelp: "\ - 您可以将该用户设置为管理员,也可以单独选择各项权限。\ - 如果选择了“管理员”,则其他的选项会被自动勾上,\ - 同时该用户可以管理其他用户。" - profileSettings: 配置文件设置 - ruleExample1: "\ - 阻止用户访问所有文件夹下任何以 . 开头的文件\ - (隐藏文件, 例如: .git, .gitignore)。" - ruleExample2: 阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。 - rules: 规则 - rulesHelp1: "\ - 您可以为该用户制定一组黑名单或白名单式的规则,\ - 被屏蔽的文件将不会显示在列表中,用户也无权限访问,\ - 支持相对于目录范围的路径。" - rulesHelp2: "\ - 每行一条规则,且必须以关键词 {0} 或 {1} 开头。\ - 如要使用正则表达式,请在加上 {2} 之后再附上表达式或路径。" - scope: 目录范围 - settingsUpdated: 设置已更新! - user: 用户 - userCommands: 用户命令(Linux 代码) - userCommandsHelp: "\ - 指定该用户可以执行的命令(Linux 代码),用空格分隔。\ - 例如:" - userCreated: 用户已创建! - userDeleted: 用户已删除! - userManagement: 用户管理 - username: 用户名 - users: 用户 - userUpdated: 用户已更新! -sidebar: - help: 帮助 - logout: 登出 - myFiles: 我的文件 - newFile: 新建文件 - newFolder: 新建文件夹 - settings: 设置 - siteSettings: 网站设置 - hugoNew: Hugo New - preview: 预览 -search: - images: 图像 - music: 音乐 - pdf: PDF - pressToExecute: 按回车键执行。 - pressToSearch: 按回车键搜索。 - search: 搜索... - searchOrCommand: 搜索或者执行命令(Linux 代码)... - searchOrSupportedCommand: 搜索或使用您可以使用的命令(一次只能执行一个命令): - type: 键入并按回车键进行搜索。 - types: 类型 - video: 视频 - writeToSearch: 请输入要搜索的内容 -languages: - en: English - fr: Français - pt: Português - ja: 日本語 - zhCN: 中文 (简体) - zhTW: 中文 (繁體) -time: - unit: 时间单位 - seconds: 秒 - minutes: 分钟 - hours: 小时 - days: 天 +permanent: 永久 +buttons: + cancel: 取消 + close: 关闭 + copy: 复制 + copyFile: 复制文件 + copyToClipboard: 复制到剪贴板 + create: 创建 + delete: 删除 + download: 下载 + info: 信息 + more: 更多 + move: 移动 + moveFile: 移动文件 + new: 新 + next: 下一个 + ok: 确定 + replace: 替换 + previous: 上一个 + rename: 重命名 + reportIssue: 报告问题 + save: 保存 + search: 搜索 + select: 选择 + share: 分享 + publish: 发布 + selectMultiple: 选择多个 + schedule: 计划 + switchView: 切换显示方式 + toggleSidebar: 切换侧边栏 + update: 更新 + upload: 上传 + permalink: 获取永久链接 +success: + linkCopied: 链接已复制! +errors: + forbidden: 你被禁止访问。 + internal: 内部出现麻烦了。 + notFound: 找不到文件。 +files: + folders: 文件夹 + files: 文件 + body: Body + clear: 清空 + closePreview: 关闭预览 + home: 主页 + lastModified: 最后修改 + loading: 加载中... + lonely: 这里没有任何文件... + metadata: 元数据 + multipleSelectionEnabled: 多选模式已开启 + name: 名称 + size: 大小 + sortByName: 按名称排序 + sortBySize: 按大小排序 + sortByLastModified: 按最后修改时间排序 +help: + click: 选择文件或目录 + ctrl: + click: 选择多个文件或目录 + f: 打开搜索框 + s: 保存文件或下载当前文件夹 + del: 删除所选的文件/文件夹 + doubleClick: 打开文件/文件夹 + esc: 清除已选项或关闭提示信息 + f1: 显示该帮助信息 + f2: 重命名文件/文件夹 + help: 帮助 +login: + password: 密码 + submit: 登录 + username: 用户名 + wrongCredentials: 用户名或密码错误 +prompts: + copy: 复制 + copyMessage: 请选择欲复制至的目录: + currentlyNavigating: 当前目录: + deleteMessageMultiple: 你确定要删除这 {count} 个文件吗? + deleteMessageSingle: 你确定要删除这个文件/文件夹吗? + deleteTitle: 删除文件 + displayName: 名称: + download: 下载文件 + downloadMessage: 请选择要下载的压缩格式。 + error: 出了一点问题... + fileInfo: 文件信息 + filesSelected: 已选择 {count} 个文件。 + lastModified: 最后修改 + move: 移动 + moveMessage: 请选择欲移动至的目录: + newDir: 新建目录 + newDirMessage: 请输入新目录的名称。 + newFile: 新建文件 + newFileMessage: 请输入新文件的名称。 + numberDirs: 目录数 + numberFiles: 文件数 + replace: 替换 + replaceMessage: "\ + 您尝试上传的文件中有一个与现有文件的名称存在冲突。\ + 是否替换现有的同名文件?" + rename: 重命名 + renameMessage: 请输入新名称,旧名称为: + show: 揭示 + size: 大小 + schedule: 计划 + scheduleMessage: 请选择发布这篇帖子的日期。 + newArchetype: 创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。 +settings: + admin: 管理员 + administrator: 管理员 + allowCommands: 执行命令(Linux 代码) + allowEdit: 编辑、重命名或删除文件/目录 + allowNew: 创建新文件和目录 + allowPublish: 发布新的帖子与页面 + avoidChanges: '(留空以避免更改)' + changePassword: 更改密码 + commands: 命令(linux 代码) + commandsHelp: "\ + 在这里,您可以设置在指定事件下执行的命令,一行一条。\ + 若事件与文件相关,如“在保存文件前”,\ + 则文件的路径会被赋值给环境变量 \"FILE\"。" + commandsUpdated: 命令已更新! + customStylesheet: 自定义样式表 + examples: 例子 + globalSettings: 全局设置 + language: 语言 + lockPassword: 禁止用户修改密码 + newPassword: 您的新密码 + newPasswordConfirm: 重输一遍新密码 + newUser: 新建用户 + password: 密码 + passwordUpdated: 密码已更新! + permissions: 权限 + permissionsHelp: "\ + 您可以将该用户设置为管理员,也可以单独选择各项权限。\ + 如果选择了“管理员”,则其他的选项会被自动勾上,\ + 同时该用户可以管理其他用户。" + profileSettings: 配置文件设置 + ruleExample1: "\ + 阻止用户访问所有文件夹下任何以 . 开头的文件\ + (隐藏文件, 例如: .git, .gitignore)。" + ruleExample2: 阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。 + rules: 规则 + rulesHelp1: "\ + 您可以为该用户制定一组黑名单或白名单式的规则,\ + 被屏蔽的文件将不会显示在列表中,用户也无权限访问,\ + 支持相对于目录范围的路径。" + rulesHelp2: "\ + 每行一条规则,且必须以关键词 {0} 或 {1} 开头。\ + 如要使用正则表达式,请在加上 {2} 之后再附上表达式或路径。" + scope: 目录范围 + settingsUpdated: 设置已更新! + user: 用户 + userCommands: 用户命令(Linux 代码) + userCommandsHelp: "\ + 指定该用户可以执行的命令(Linux 代码),用空格分隔。\ + 例如:" + userCreated: 用户已创建! + userDeleted: 用户已删除! + userManagement: 用户管理 + username: 用户名 + users: 用户 + userUpdated: 用户已更新! +sidebar: + help: 帮助 + logout: 登出 + myFiles: 我的文件 + newFile: 新建文件 + newFolder: 新建文件夹 + settings: 设置 + siteSettings: 网站设置 + hugoNew: Hugo New + preview: 预览 +search: + images: 图像 + music: 音乐 + pdf: PDF + pressToExecute: 按回车键执行。 + pressToSearch: 按回车键搜索。 + search: 搜索... + searchOrCommand: 搜索或者执行命令(Linux 代码)... + searchOrSupportedCommand: 搜索或使用您可以使用的命令(一次只能执行一个命令): + type: 键入并按回车键进行搜索。 + types: 类型 + video: 视频 + writeToSearch: 请输入要搜索的内容 +languages: + en: English + fr: Français + pt: Português + ja: 日本語 + zhCN: 中文 (简体) + zhTW: 中文 (繁體) +time: + unit: 时间单位 + seconds: 秒 + minutes: 分钟 + hours: 小时 + days: 天 diff --git a/assets/src/utils/codemirror.js b/assets/src/utils/codemirror.js index e40ba58c4c..d8c574fc85 100644 --- a/assets/src/utils/codemirror.js +++ b/assets/src/utils/codemirror.js @@ -1,60 +1,60 @@ -// Most of the code from this file comes from: -// https://github.com/codemirror/CodeMirror/blob/master/addon/mode/loadmode.js -import * as CodeMirror from 'codemirror' -import store from '@/store' - -// Make CodeMirror available globally so the modes' can register themselves. -window.CodeMirror = CodeMirror -CodeMirror.modeURL = store.state.baseURL + '/static/js/codemirror/mode/%N/%N.js' - -var loading = {} - -function splitCallback (cont, n) { - var countDown = n - return function () { - if (--countDown === 0) cont() - } -} - -function ensureDeps (mode, cont) { - var deps = CodeMirror.modes[mode].dependencies - if (!deps) return cont() - var missing = [] - for (var i = 0; i < deps.length; ++i) { - if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i]) - } - if (!missing.length) return cont() - var split = splitCallback(cont, missing.length) - for (i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split) -} - -CodeMirror.requireMode = function (mode, cont) { - if (typeof mode !== 'string') mode = mode.name - if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont) - if (loading.hasOwnProperty(mode)) return loading[mode].push(cont) - - var file = CodeMirror.modeURL.replace(/%N/g, mode) - - var script = document.createElement('script') - script.src = file - var others = document.getElementsByTagName('script')[0] - var list = loading[mode] = [cont] - - CodeMirror.on(script, 'load', function () { - ensureDeps(mode, function () { - for (var i = 0; i < list.length; ++i) list[i]() - }) - }) - - others.parentNode.insertBefore(script, others) -} - -CodeMirror.autoLoadMode = function (instance, mode) { - if (CodeMirror.modes.hasOwnProperty(mode)) return - - CodeMirror.requireMode(mode, function () { - instance.setOption('mode', mode) - }) -} - -export default CodeMirror +// Most of the code from this file comes from: +// https://github.com/codemirror/CodeMirror/blob/master/addon/mode/loadmode.js +import * as CodeMirror from 'codemirror' +import store from '@/store' + +// Make CodeMirror available globally so the modes' can register themselves. +window.CodeMirror = CodeMirror +CodeMirror.modeURL = store.state.baseURL + '/static/js/codemirror/mode/%N/%N.js' + +var loading = {} + +function splitCallback (cont, n) { + var countDown = n + return function () { + if (--countDown === 0) cont() + } +} + +function ensureDeps (mode, cont) { + var deps = CodeMirror.modes[mode].dependencies + if (!deps) return cont() + var missing = [] + for (var i = 0; i < deps.length; ++i) { + if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i]) + } + if (!missing.length) return cont() + var split = splitCallback(cont, missing.length) + for (i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split) +} + +CodeMirror.requireMode = function (mode, cont) { + if (typeof mode !== 'string') mode = mode.name + if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont) + if (loading.hasOwnProperty(mode)) return loading[mode].push(cont) + + var file = CodeMirror.modeURL.replace(/%N/g, mode) + + var script = document.createElement('script') + script.src = file + var others = document.getElementsByTagName('script')[0] + var list = loading[mode] = [cont] + + CodeMirror.on(script, 'load', function () { + ensureDeps(mode, function () { + for (var i = 0; i < list.length; ++i) list[i]() + }) + }) + + others.parentNode.insertBefore(script, others) +} + +CodeMirror.autoLoadMode = function (instance, mode) { + if (CodeMirror.modes.hasOwnProperty(mode)) return + + CodeMirror.requireMode(mode, function () { + instance.setOption('mode', mode) + }) +} + +export default CodeMirror diff --git a/assets/src/utils/cookie.js b/assets/src/utils/cookie.js index 5004b6021d..5b6b2a864a 100644 --- a/assets/src/utils/cookie.js +++ b/assets/src/utils/cookie.js @@ -1,4 +1,4 @@ -export default function (name) { - let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$') - return document.cookie.replace(re, '$1') -} +export default function (name) { + let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$') + return document.cookie.replace(re, '$1') +} diff --git a/assets/src/utils/css.js b/assets/src/utils/css.js index 15ab99fe56..eea624722b 100644 --- a/assets/src/utils/css.js +++ b/assets/src/utils/css.js @@ -1,28 +1,28 @@ -export default function getRule (rules) { - for (let i = 0; i < rules.length; i++) { - rules[i] = rules[i].toLowerCase() - } - - let result = null - let find = Array.prototype.find - - find.call(document.styleSheets, styleSheet => { - result = find.call(styleSheet.cssRules, cssRule => { - let found = false - - if (cssRule instanceof window.CSSStyleRule) { - for (let i = 0; i < rules.length; i++) { - if (cssRule.selectorText.toLowerCase() === rules[i]) { - found = true - } - } - } - - return found - }) - - return result != null - }) - - return result -} +export default function getRule (rules) { + for (let i = 0; i < rules.length; i++) { + rules[i] = rules[i].toLowerCase() + } + + let result = null + let find = Array.prototype.find + + find.call(document.styleSheets, styleSheet => { + result = find.call(styleSheet.cssRules, cssRule => { + let found = false + + if (cssRule instanceof window.CSSStyleRule) { + for (let i = 0; i < rules.length; i++) { + if (cssRule.selectorText.toLowerCase() === rules[i]) { + found = true + } + } + } + + return found + }) + + return result != null + }) + + return result +} diff --git a/assets/src/utils/url.js b/assets/src/utils/url.js index 2649a59236..832314fd53 100644 --- a/assets/src/utils/url.js +++ b/assets/src/utils/url.js @@ -1,12 +1,12 @@ -function removeLastDir (url) { - var arr = url.split('/') - if (arr.pop() === '') { - arr.pop() - } - - return arr.join('/') -} - -export default { - removeLastDir: removeLastDir -} +function removeLastDir (url) { + var arr = url.split('/') + if (arr.pop() === '') { + arr.pop() + } + + return arr.join('/') +} + +export default { + removeLastDir: removeLastDir +} diff --git a/assets/src/views/Files.vue b/assets/src/views/Files.vue index 8d8b3a160b..d72bafced4 100644 --- a/assets/src/views/Files.vue +++ b/assets/src/views/Files.vue @@ -1,231 +1,231 @@ - - - + + + diff --git a/assets/src/views/errors/403.vue b/assets/src/views/errors/403.vue index 47c6c8978c..5c7d9489cb 100644 --- a/assets/src/views/errors/403.vue +++ b/assets/src/views/errors/403.vue @@ -1,13 +1,13 @@ - - - - + + + + diff --git a/assets/src/views/errors/404.vue b/assets/src/views/errors/404.vue index 61dbe14419..66961fccee 100644 --- a/assets/src/views/errors/404.vue +++ b/assets/src/views/errors/404.vue @@ -1,13 +1,13 @@ - - - - + + + + diff --git a/assets/src/views/errors/500.vue b/assets/src/views/errors/500.vue index 0bd86786d1..f3d4a195c6 100644 --- a/assets/src/views/errors/500.vue +++ b/assets/src/views/errors/500.vue @@ -1,13 +1,13 @@ - - - - + + + + diff --git a/assets/static/manifest.json b/assets/static/manifest.json index 25bd2d981f..5e776eba76 100644 --- a/assets/static/manifest.json +++ b/assets/static/manifest.json @@ -1,20 +1,20 @@ -{ - "name": "File Manager", - "short_name": "File Manager", - "icons": [ - { - "src": "{{ .BaseURL }}/static/img/icons/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "{{ .BaseURL }}/static/img/icons/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "start_url": "{{ .BaseURL }}/", - "display": "standalone", - "background_color": "#ffffff", - "theme_color": "#2979ff" -} +{ + "name": "File Manager", + "short_name": "File Manager", + "icons": [ + { + "src": "{{ .BaseURL }}/static/img/icons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "{{ .BaseURL }}/static/img/icons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "{{ .BaseURL }}/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#2979ff" +} diff --git a/assets/static/share/404.html b/assets/static/share/404.html index 0136dd0ecd..155804a90a 100644 --- a/assets/static/share/404.html +++ b/assets/static/share/404.html @@ -1,50 +1,50 @@ - - - - - - - File Manager - - - - - - - - - - - - - - - - -

404 Not Found

- - + + + + + + + File Manager + + + + + + + + + + + + + + + + +

404 Not Found

+ + diff --git a/assets/static/share/index.html b/assets/static/share/index.html index 44e2f0abc7..629bbf7330 100644 --- a/assets/static/share/index.html +++ b/assets/static/share/index.html @@ -1,85 +1,85 @@ - - - - - - - {{ .File.Name }} - - - - - - - - - - - - - - - - - -
Download {{ if .File.IsDir }}Folder{{ else }}File{{ end }}
-
- {{ if .File.IsDir -}} - - - - - {{ else -}} - - - - - {{ end -}} -

{{ .File.Name }}

-
-
- - + + + + + + + {{ .File.Name }} + + + + + + + + + + + + + + + + + +
Download {{ if .File.IsDir }}Folder{{ else }}File{{ end }}
+
+ {{ if .File.IsDir -}} + + + + + {{ else -}} + + + + + {{ end -}} +

{{ .File.Name }}

+
+
+ + diff --git a/bolt/config.go b/bolt/config.go index e3d7fb81ae..f74e90ee25 100644 --- a/bolt/config.go +++ b/bolt/config.go @@ -1,26 +1,26 @@ -package bolt - -import ( - "github.com/asdine/storm" - fm "github.com/hacdias/filemanager" -) - -// ConfigStore is a configuration store. -type ConfigStore struct { - DB *storm.DB -} - -// Get gets a configuration from the database to an interface. -func (c ConfigStore) Get(name string, to interface{}) error { - err := c.DB.Get("config", name, to) - if err == storm.ErrNotFound { - return fm.ErrNotExist - } - - return err -} - -// Save saves a configuration from an interface to the database. -func (c ConfigStore) Save(name string, from interface{}) error { - return c.DB.Set("config", name, from) -} +package bolt + +import ( + "github.com/asdine/storm" + fm "github.com/hacdias/filemanager" +) + +// ConfigStore is a configuration store. +type ConfigStore struct { + DB *storm.DB +} + +// Get gets a configuration from the database to an interface. +func (c ConfigStore) Get(name string, to interface{}) error { + err := c.DB.Get("config", name, to) + if err == storm.ErrNotFound { + return fm.ErrNotExist + } + + return err +} + +// Save saves a configuration from an interface to the database. +func (c ConfigStore) Save(name string, from interface{}) error { + return c.DB.Set("config", name, from) +} diff --git a/bolt/users.go b/bolt/users.go index 6189016b0e..4ee3a0e1a0 100644 --- a/bolt/users.go +++ b/bolt/users.go @@ -1,90 +1,90 @@ -package bolt - -import ( - "reflect" - - "github.com/asdine/storm" - fm "github.com/hacdias/filemanager" -) - -// UsersStore is a users store. -type UsersStore struct { - DB *storm.DB -} - -// Get gets a user with a certain id from the database. -func (u UsersStore) Get(id int, builder fm.FSBuilder) (*fm.User, error) { - var us fm.User - err := u.DB.One("ID", id, &us) - if err == storm.ErrNotFound { - return nil, fm.ErrNotExist - } - - if err != nil { - return nil, err - } - - us.FileSystem = builder(us.Scope) - return &us, nil -} - -// GetByUsername gets a user with a certain username from the database. -func (u UsersStore) GetByUsername(username string, builder fm.FSBuilder) (*fm.User, error) { - var us fm.User - err := u.DB.One("Username", username, &us) - if err == storm.ErrNotFound { - return nil, fm.ErrNotExist - } - - if err != nil { - return nil, err - } - - us.FileSystem = builder(us.Scope) - return &us, nil -} - -// Gets gets all the users from the database. -func (u UsersStore) Gets(builder fm.FSBuilder) ([]*fm.User, error) { - var us []*fm.User - err := u.DB.All(&us) - if err == storm.ErrNotFound { - return nil, fm.ErrNotExist - } - - if err != nil { - return us, err - } - - for _, user := range us { - user.FileSystem = builder(user.Scope) - } - - return us, err -} - -// Update updates the whole user object or only certain fields. -func (u UsersStore) Update(us *fm.User, fields ...string) error { - if len(fields) == 0 { - return u.Save(us) - } - - for _, field := range fields { - val := reflect.ValueOf(us).Elem().FieldByName(field).Interface() - if err := u.DB.UpdateField(us, field, val); err != nil { - return err - } - } - - return nil -} - -// Save saves a user to the database. -func (u UsersStore) Save(us *fm.User) error { - return u.DB.Save(us) -} - -// Delete deletes a user from the database. -func (u UsersStore) Delete(id int) error { - return u.DB.DeleteStruct(&fm.User{ID: id}) -} +package bolt + +import ( + "reflect" + + "github.com/asdine/storm" + fm "github.com/hacdias/filemanager" +) + +// UsersStore is a users store. +type UsersStore struct { + DB *storm.DB +} + +// Get gets a user with a certain id from the database. +func (u UsersStore) Get(id int, builder fm.FSBuilder) (*fm.User, error) { + var us fm.User + err := u.DB.One("ID", id, &us) + if err == storm.ErrNotFound { + return nil, fm.ErrNotExist + } + + if err != nil { + return nil, err + } + + us.FileSystem = builder(us.Scope) + return &us, nil +} + +// GetByUsername gets a user with a certain username from the database. +func (u UsersStore) GetByUsername(username string, builder fm.FSBuilder) (*fm.User, error) { + var us fm.User + err := u.DB.One("Username", username, &us) + if err == storm.ErrNotFound { + return nil, fm.ErrNotExist + } + + if err != nil { + return nil, err + } + + us.FileSystem = builder(us.Scope) + return &us, nil +} + +// Gets gets all the users from the database. +func (u UsersStore) Gets(builder fm.FSBuilder) ([]*fm.User, error) { + var us []*fm.User + err := u.DB.All(&us) + if err == storm.ErrNotFound { + return nil, fm.ErrNotExist + } + + if err != nil { + return us, err + } + + for _, user := range us { + user.FileSystem = builder(user.Scope) + } + + return us, err +} + +// Update updates the whole user object or only certain fields. +func (u UsersStore) Update(us *fm.User, fields ...string) error { + if len(fields) == 0 { + return u.Save(us) + } + + for _, field := range fields { + val := reflect.ValueOf(us).Elem().FieldByName(field).Interface() + if err := u.DB.UpdateField(us, field, val); err != nil { + return err + } + } + + return nil +} + +// Save saves a user to the database. +func (u UsersStore) Save(us *fm.User) error { + return u.DB.Save(us) +} + +// Delete deletes a user from the database. +func (u UsersStore) Delete(id int) error { + return u.DB.DeleteStruct(&fm.User{ID: id}) +} diff --git a/build.sh b/build.sh index f38a139f29..813f74fe6d 100644 --- a/build.sh +++ b/build.sh @@ -1,13 +1,13 @@ -#!/bin/bash - -# Install rice tool if not present -if ! [ -x "$(command -v rice)" ]; then - go get github.com/GeertJohan/go.rice/rice -fi - -# Clean the dist folder and build the assets -rm -rf assets/dist -npm run build - -# Embed the assets using rice -rice embed-go +#!/bin/bash + +# Install rice tool if not present +if ! [ -x "$(command -v rice)" ]; then + go get github.com/GeertJohan/go.rice/rice +fi + +# Clean the dist folder and build the assets +rm -rf assets/dist +npm run build + +# Embed the assets using rice +rice embed-go diff --git a/caddy/filemanager/filemanager.go b/caddy/filemanager/filemanager.go index 979854cc19..cf02ca8084 100644 --- a/caddy/filemanager/filemanager.go +++ b/caddy/filemanager/filemanager.go @@ -1,55 +1,55 @@ -// Package filemanager provides middleware for managing files in a directory -// when directory path is requested instead of a specific file. Based on browse -// middleware. -package filemanager - -import ( - "net/http" - - "github.com/hacdias/filemanager" - "github.com/hacdias/filemanager/caddy/parser" - h "github.com/hacdias/filemanager/http" - "github.com/mholt/caddy" - "github.com/mholt/caddy/caddyhttp/httpserver" -) - -func init() { - caddy.RegisterPlugin("filemanager", caddy.Plugin{ - ServerType: "http", - Action: setup, - }) -} - -type plugin struct { - Next httpserver.Handler - Configs []*filemanager.FileManager -} - -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - for i := range f.Configs { - // Checks if this Path should be handled by File Manager. - if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { - continue - } - - h.Handler(f.Configs[i]).ServeHTTP(w, r) - return 0, nil - } - - return f.Next.ServeHTTP(w, r) -} - -// setup configures a new FileManager middleware instance. -func setup(c *caddy.Controller) error { - configs, err := parser.Parse(c, "") - if err != nil { - return err - } - - httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { - return plugin{Configs: configs, Next: next} - }) - - return nil -} +// Package filemanager provides middleware for managing files in a directory +// when directory path is requested instead of a specific file. Based on browse +// middleware. +package filemanager + +import ( + "net/http" + + "github.com/hacdias/filemanager" + "github.com/hacdias/filemanager/caddy/parser" + h "github.com/hacdias/filemanager/http" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func init() { + caddy.RegisterPlugin("filemanager", caddy.Plugin{ + ServerType: "http", + Action: setup, + }) +} + +type plugin struct { + Next httpserver.Handler + Configs []*filemanager.FileManager +} + +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for i := range f.Configs { + // Checks if this Path should be handled by File Manager. + if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { + continue + } + + h.Handler(f.Configs[i]).ServeHTTP(w, r) + return 0, nil + } + + return f.Next.ServeHTTP(w, r) +} + +// setup configures a new FileManager middleware instance. +func setup(c *caddy.Controller) error { + configs, err := parser.Parse(c, "") + if err != nil { + return err + } + + httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return plugin{Configs: configs, Next: next} + }) + + return nil +} diff --git a/caddy/hugo/hugo.go b/caddy/hugo/hugo.go index dedb24cef1..3badc83d77 100644 --- a/caddy/hugo/hugo.go +++ b/caddy/hugo/hugo.go @@ -1,52 +1,52 @@ -package hugo - -import ( - "net/http" - - "github.com/hacdias/filemanager" - "github.com/hacdias/filemanager/caddy/parser" - h "github.com/hacdias/filemanager/http" - "github.com/mholt/caddy" - "github.com/mholt/caddy/caddyhttp/httpserver" -) - -func init() { - caddy.RegisterPlugin("hugo", caddy.Plugin{ - ServerType: "http", - Action: setup, - }) -} - -type plugin struct { - Next httpserver.Handler - Configs []*filemanager.FileManager -} - -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - for i := range f.Configs { - // Checks if this Path should be handled by File Manager. - if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { - continue - } - - h.Handler(f.Configs[i]).ServeHTTP(w, r) - return 0, nil - } - - return f.Next.ServeHTTP(w, r) -} - -// setup configures a new FileManager middleware instance. -func setup(c *caddy.Controller) error { - configs, err := parser.Parse(c, "hugo") - if err != nil { - return err - } - - httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { - return plugin{Configs: configs, Next: next} - }) - - return nil -} +package hugo + +import ( + "net/http" + + "github.com/hacdias/filemanager" + "github.com/hacdias/filemanager/caddy/parser" + h "github.com/hacdias/filemanager/http" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func init() { + caddy.RegisterPlugin("hugo", caddy.Plugin{ + ServerType: "http", + Action: setup, + }) +} + +type plugin struct { + Next httpserver.Handler + Configs []*filemanager.FileManager +} + +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for i := range f.Configs { + // Checks if this Path should be handled by File Manager. + if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { + continue + } + + h.Handler(f.Configs[i]).ServeHTTP(w, r) + return 0, nil + } + + return f.Next.ServeHTTP(w, r) +} + +// setup configures a new FileManager middleware instance. +func setup(c *caddy.Controller) error { + configs, err := parser.Parse(c, "hugo") + if err != nil { + return err + } + + httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return plugin{Configs: configs, Next: next} + }) + + return nil +} diff --git a/caddy/jekyll/jekyll.go b/caddy/jekyll/jekyll.go index b41d4f5ed6..e79948d8c6 100644 --- a/caddy/jekyll/jekyll.go +++ b/caddy/jekyll/jekyll.go @@ -1,52 +1,52 @@ -package jekyll - -import ( - "net/http" - - "github.com/hacdias/filemanager" - "github.com/hacdias/filemanager/caddy/parser" - h "github.com/hacdias/filemanager/http" - "github.com/mholt/caddy" - "github.com/mholt/caddy/caddyhttp/httpserver" -) - -func init() { - caddy.RegisterPlugin("jekyll", caddy.Plugin{ - ServerType: "http", - Action: setup, - }) -} - -type plugin struct { - Next httpserver.Handler - Configs []*filemanager.FileManager -} - -// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. -func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - for i := range f.Configs { - // Checks if this Path should be handled by File Manager. - if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { - continue - } - - h.Handler(f.Configs[i]).ServeHTTP(w, r) - return 0, nil - } - - return f.Next.ServeHTTP(w, r) -} - -// setup configures a new FileManager middleware instance. -func setup(c *caddy.Controller) error { - configs, err := parser.Parse(c, "jekyll") - if err != nil { - return err - } - - httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { - return plugin{Configs: configs, Next: next} - }) - - return nil -} +package jekyll + +import ( + "net/http" + + "github.com/hacdias/filemanager" + "github.com/hacdias/filemanager/caddy/parser" + h "github.com/hacdias/filemanager/http" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func init() { + caddy.RegisterPlugin("jekyll", caddy.Plugin{ + ServerType: "http", + Action: setup, + }) +} + +type plugin struct { + Next httpserver.Handler + Configs []*filemanager.FileManager +} + +// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. +func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for i := range f.Configs { + // Checks if this Path should be handled by File Manager. + if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { + continue + } + + h.Handler(f.Configs[i]).ServeHTTP(w, r) + return 0, nil + } + + return f.Next.ServeHTTP(w, r) +} + +// setup configures a new FileManager middleware instance. +func setup(c *caddy.Controller) error { + configs, err := parser.Parse(c, "jekyll") + if err != nil { + return err + } + + httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return plugin{Configs: configs, Next: next} + }) + + return nil +} diff --git a/caddy/parser/parser.go b/caddy/parser/parser.go index aa295c284c..6a36c9b11b 100644 --- a/caddy/parser/parser.go +++ b/caddy/parser/parser.go @@ -1,294 +1,294 @@ -package parser - -import ( - "crypto/md5" - "encoding/hex" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/asdine/storm" - "github.com/hacdias/filemanager" - "github.com/hacdias/filemanager/bolt" - "github.com/hacdias/filemanager/staticgen" - "github.com/hacdias/fileutils" - "github.com/mholt/caddy" - "github.com/mholt/caddy/caddyhttp/httpserver" -) - -var databases = map[string]*storm.DB{} - -// Parse ... -func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) { - var ( - configs []*filemanager.FileManager - err error - ) - - for c.Next() { - u := &filemanager.User{ - Locale: "en", - AllowCommands: true, - AllowEdit: true, - AllowNew: true, - AllowPublish: true, - Commands: []string{"git", "svn", "hg"}, - CSS: "", - Rules: []*filemanager.Rule{{ - Regex: true, - Allow: false, - Regexp: &filemanager.Regexp{Raw: "\\/\\..+"}, - }}, - } - - baseURL := "/" - scope := "." - database := "" - noAuth := false - reCaptchaKey := "" - reCaptchaSecret := "" - - if plugin != "" { - baseURL = "/admin" - } - - // Get the baseURL and scope - args := c.RemainingArgs() - - if plugin == "" { - if len(args) >= 1 { - baseURL = args[0] - } - - if len(args) > 1 { - scope = args[1] - } - } else { - if len(args) >= 1 { - scope = args[0] - } - - if len(args) > 1 { - baseURL = args[1] - } - } - - for c.NextBlock() { - switch c.Val() { - case "database": - if !c.NextArg() { - return nil, c.ArgErr() - } - - database = c.Val() - case "locale": - if !c.NextArg() { - return nil, c.ArgErr() - } - - u.Locale = c.Val() - case "allow_commands": - if !c.NextArg() { - u.AllowCommands = true - continue - } - - u.AllowCommands, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - case "allow_edit": - if !c.NextArg() { - u.AllowEdit = true - continue - } - - u.AllowEdit, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - case "allow_new": - if !c.NextArg() { - u.AllowNew = true - continue - } - - u.AllowNew, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - case "allow_publish": - if !c.NextArg() { - u.AllowPublish = true - continue - } - - u.AllowPublish, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - case "commands": - if !c.NextArg() { - return nil, c.ArgErr() - } - - u.Commands = strings.Split(c.Val(), " ") - case "css": - if !c.NextArg() { - return nil, c.ArgErr() - } - - file := c.Val() - css, err := ioutil.ReadFile(file) - if err != nil { - return nil, err - } - - u.CSS = string(css) - case "view_mode": - if !c.NextArg() { - return nil, c.ArgErr() - } - - u.ViewMode = c.Val() - if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode { - return nil, c.ArgErr() - } - case "recaptcha_key": - if !c.NextArg() { - return nil, c.ArgErr() - } - - reCaptchaKey = c.Val() - case "recaptcha_secret": - if !c.NextArg() { - return nil, c.ArgErr() - } - - reCaptchaSecret = c.Val() - case "no_auth": - if !c.NextArg() { - noAuth = true - continue - } - - noAuth, err = strconv.ParseBool(c.Val()) - if err != nil { - return nil, err - } - } - } - - caddyConf := httpserver.GetConfig(c) - - path := filepath.Join(caddy.AssetsPath(), "filemanager") - err := os.MkdirAll(path, 0700) - if err != nil { - return nil, err - } - - // if there is a database path and it is not absolute, - // it will be relative to Caddy folder. - if !filepath.IsAbs(database) && database != "" { - database = filepath.Join(path, database) - } - - // If there is no database path on the settings, - // store one in .caddy/filemanager/name.db. - if database == "" { - // The name of the database is the hashed value of a string composed - // by the host, address path and the baseurl of this File Manager - // instance. - hasher := md5.New() - hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL)) - sha := hex.EncodeToString(hasher.Sum(nil)) - database = filepath.Join(path, sha+".db") - - fmt.Println("[WARNING] A database is going to be created for your File Manager instance at " + database + - ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n") - } - - u.Scope = scope - u.FileSystem = fileutils.Dir(scope) - - var db *storm.DB - if stored, ok := databases[database]; ok { - db = stored - } else { - db, err = storm.Open(database) - databases[database] = db - } - - if err != nil { - return nil, err - } - - m := &filemanager.FileManager{ - NoAuth: noAuth, - BaseURL: "", - PrefixURL: "", - ReCaptchaKey: reCaptchaKey, - ReCaptchaSecret: reCaptchaSecret, - DefaultUser: u, - Store: &filemanager.Store{ - Config: bolt.ConfigStore{DB: db}, - Users: bolt.UsersStore{DB: db}, - Share: bolt.ShareStore{DB: db}, - }, - NewFS: func(scope string) filemanager.FileSystem { - return fileutils.Dir(scope) - }, - } - - err = m.Setup() - if err != nil { - return nil, err - } - - switch plugin { - case "hugo": - // Initialize the default settings for Hugo. - hugo := &staticgen.Hugo{ - Root: scope, - Public: filepath.Join(scope, "public"), - Args: []string{}, - CleanPublic: true, - } - - // Attaches Hugo plugin to this file manager instance. - err = m.Attach(hugo) - if err != nil { - return nil, err - } - case "jekyll": - // Initialize the default settings for Jekyll. - jekyll := &staticgen.Jekyll{ - Root: scope, - Public: filepath.Join(scope, "_site"), - Args: []string{}, - CleanPublic: true, - } - - // Attaches Hugo plugin to this file manager instance. - err = m.Attach(jekyll) - if err != nil { - return nil, err - } - } - - if err != nil { - return nil, err - } - - m.NoAuth = noAuth - m.SetBaseURL(baseURL) - m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) - - configs = append(configs, m) - } - - return configs, nil -} +package parser + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/asdine/storm" + "github.com/hacdias/filemanager" + "github.com/hacdias/filemanager/bolt" + "github.com/hacdias/filemanager/staticgen" + "github.com/hacdias/fileutils" + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +var databases = map[string]*storm.DB{} + +// Parse ... +func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) { + var ( + configs []*filemanager.FileManager + err error + ) + + for c.Next() { + u := &filemanager.User{ + Locale: "en", + AllowCommands: true, + AllowEdit: true, + AllowNew: true, + AllowPublish: true, + Commands: []string{"git", "svn", "hg"}, + CSS: "", + Rules: []*filemanager.Rule{{ + Regex: true, + Allow: false, + Regexp: &filemanager.Regexp{Raw: "\\/\\..+"}, + }}, + } + + baseURL := "/" + scope := "." + database := "" + noAuth := false + reCaptchaKey := "" + reCaptchaSecret := "" + + if plugin != "" { + baseURL = "/admin" + } + + // Get the baseURL and scope + args := c.RemainingArgs() + + if plugin == "" { + if len(args) >= 1 { + baseURL = args[0] + } + + if len(args) > 1 { + scope = args[1] + } + } else { + if len(args) >= 1 { + scope = args[0] + } + + if len(args) > 1 { + baseURL = args[1] + } + } + + for c.NextBlock() { + switch c.Val() { + case "database": + if !c.NextArg() { + return nil, c.ArgErr() + } + + database = c.Val() + case "locale": + if !c.NextArg() { + return nil, c.ArgErr() + } + + u.Locale = c.Val() + case "allow_commands": + if !c.NextArg() { + u.AllowCommands = true + continue + } + + u.AllowCommands, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + case "allow_edit": + if !c.NextArg() { + u.AllowEdit = true + continue + } + + u.AllowEdit, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + case "allow_new": + if !c.NextArg() { + u.AllowNew = true + continue + } + + u.AllowNew, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + case "allow_publish": + if !c.NextArg() { + u.AllowPublish = true + continue + } + + u.AllowPublish, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + case "commands": + if !c.NextArg() { + return nil, c.ArgErr() + } + + u.Commands = strings.Split(c.Val(), " ") + case "css": + if !c.NextArg() { + return nil, c.ArgErr() + } + + file := c.Val() + css, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + u.CSS = string(css) + case "view_mode": + if !c.NextArg() { + return nil, c.ArgErr() + } + + u.ViewMode = c.Val() + if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode { + return nil, c.ArgErr() + } + case "recaptcha_key": + if !c.NextArg() { + return nil, c.ArgErr() + } + + reCaptchaKey = c.Val() + case "recaptcha_secret": + if !c.NextArg() { + return nil, c.ArgErr() + } + + reCaptchaSecret = c.Val() + case "no_auth": + if !c.NextArg() { + noAuth = true + continue + } + + noAuth, err = strconv.ParseBool(c.Val()) + if err != nil { + return nil, err + } + } + } + + caddyConf := httpserver.GetConfig(c) + + path := filepath.Join(caddy.AssetsPath(), "filemanager") + err := os.MkdirAll(path, 0700) + if err != nil { + return nil, err + } + + // if there is a database path and it is not absolute, + // it will be relative to Caddy folder. + if !filepath.IsAbs(database) && database != "" { + database = filepath.Join(path, database) + } + + // If there is no database path on the settings, + // store one in .caddy/filemanager/name.db. + if database == "" { + // The name of the database is the hashed value of a string composed + // by the host, address path and the baseurl of this File Manager + // instance. + hasher := md5.New() + hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL)) + sha := hex.EncodeToString(hasher.Sum(nil)) + database = filepath.Join(path, sha+".db") + + fmt.Println("[WARNING] A database is going to be created for your File Manager instance at " + database + + ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n") + } + + u.Scope = scope + u.FileSystem = fileutils.Dir(scope) + + var db *storm.DB + if stored, ok := databases[database]; ok { + db = stored + } else { + db, err = storm.Open(database) + databases[database] = db + } + + if err != nil { + return nil, err + } + + m := &filemanager.FileManager{ + NoAuth: noAuth, + BaseURL: "", + PrefixURL: "", + ReCaptchaKey: reCaptchaKey, + ReCaptchaSecret: reCaptchaSecret, + DefaultUser: u, + Store: &filemanager.Store{ + Config: bolt.ConfigStore{DB: db}, + Users: bolt.UsersStore{DB: db}, + Share: bolt.ShareStore{DB: db}, + }, + NewFS: func(scope string) filemanager.FileSystem { + return fileutils.Dir(scope) + }, + } + + err = m.Setup() + if err != nil { + return nil, err + } + + switch plugin { + case "hugo": + // Initialize the default settings for Hugo. + hugo := &staticgen.Hugo{ + Root: scope, + Public: filepath.Join(scope, "public"), + Args: []string{}, + CleanPublic: true, + } + + // Attaches Hugo plugin to this file manager instance. + err = m.Attach(hugo) + if err != nil { + return nil, err + } + case "jekyll": + // Initialize the default settings for Jekyll. + jekyll := &staticgen.Jekyll{ + Root: scope, + Public: filepath.Join(scope, "_site"), + Args: []string{}, + CleanPublic: true, + } + + // Attaches Hugo plugin to this file manager instance. + err = m.Attach(jekyll) + if err != nil { + return nil, err + } + } + + if err != nil { + return nil, err + } + + m.NoAuth = noAuth + m.SetBaseURL(baseURL) + m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) + + configs = append(configs, m) + } + + return configs, nil +} diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go index 92112df811..ba03eef6de 100644 --- a/cmd/filemanager/main.go +++ b/cmd/filemanager/main.go @@ -1,249 +1,249 @@ -package main - -import ( - "fmt" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/asdine/storm" - - lumberjack "gopkg.in/natefinch/lumberjack.v2" - - "github.com/hacdias/filemanager" - "github.com/hacdias/filemanager/bolt" - h "github.com/hacdias/filemanager/http" - "github.com/hacdias/filemanager/staticgen" - "github.com/hacdias/fileutils" - flag "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -var ( - addr string - config string - database string - scope string - commands string - logfile string - staticg string - locale string - baseurl string - prefixurl string - viewMode string - recaptchakey string - recaptchasecret string - port int - noAuth bool - allowCommands bool - allowEdit bool - allowNew bool - allowPublish bool - showVer bool -) - -func init() { - flag.StringVarP(&config, "config", "c", "", "Configuration file") - flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)") - flag.StringVarP(&addr, "address", "a", "", "Address to listen to (default is all of them)") - flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file") - flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file") - flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users") - flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL") - flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users") - flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL") - flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users") - flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key") - flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret") - flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") - flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users") - flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users") - flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users") - flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication") - flag.StringVar(&locale, "locale", "", "Default locale for new users, set it empty to enable auto detect from browser") - flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable") - flag.BoolVarP(&showVer, "version", "v", false, "Show version") -} - -func setupViper() { - viper.SetDefault("Address", "") - viper.SetDefault("Port", "0") - viper.SetDefault("Database", "./filemanager.db") - viper.SetDefault("Scope", ".") - viper.SetDefault("Logger", "stdout") - viper.SetDefault("Commands", []string{"git", "svn", "hg"}) - viper.SetDefault("AllowCommmands", true) - viper.SetDefault("AllowEdit", true) - viper.SetDefault("AllowNew", true) - viper.SetDefault("AllowPublish", true) - viper.SetDefault("StaticGen", "") - viper.SetDefault("Locale", "") - viper.SetDefault("NoAuth", false) - viper.SetDefault("BaseURL", "") - viper.SetDefault("PrefixURL", "") - viper.SetDefault("ViewMode", filemanager.MosaicViewMode) - viper.SetDefault("ReCaptchaKey", "") - viper.SetDefault("ReCaptchaSecret", "") - - viper.BindPFlag("Port", flag.Lookup("port")) - viper.BindPFlag("Address", flag.Lookup("address")) - viper.BindPFlag("Database", flag.Lookup("database")) - viper.BindPFlag("Scope", flag.Lookup("scope")) - viper.BindPFlag("Logger", flag.Lookup("log")) - viper.BindPFlag("Commands", flag.Lookup("commands")) - viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands")) - viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit")) - viper.BindPFlag("AlowNew", flag.Lookup("allow-new")) - viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish")) - viper.BindPFlag("Locale", flag.Lookup("locale")) - viper.BindPFlag("StaticGen", flag.Lookup("staticgen")) - viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) - viper.BindPFlag("BaseURL", flag.Lookup("baseurl")) - viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl")) - viper.BindPFlag("ViewMode", flag.Lookup("view-mode")) - viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key")) - viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret")) - - viper.SetConfigName("filemanager") - viper.AddConfigPath(".") -} - -func printVersion() { - fmt.Println("filemanager version", filemanager.Version) - os.Exit(0) -} - -func main() { - setupViper() - flag.Parse() - - if showVer { - printVersion() - } - - // Add a configuration file if set. - if config != "" { - ext := filepath.Ext(config) - dir := filepath.Dir(config) - config = strings.TrimSuffix(config, ext) - - if dir != "" { - viper.AddConfigPath(dir) - config = strings.TrimPrefix(config, dir) - } - - viper.SetConfigName(config) - } - - // Read configuration from a file if exists. - err := viper.ReadInConfig() - if err != nil { - if _, ok := err.(viper.ConfigParseError); ok { - panic(err) - } - } - - // Set up process log before anything bad happens. - switch viper.GetString("Logger") { - case "stdout": - log.SetOutput(os.Stdout) - case "stderr": - log.SetOutput(os.Stderr) - case "": - log.SetOutput(ioutil.Discard) - default: - log.SetOutput(&lumberjack.Logger{ - Filename: logfile, - MaxSize: 100, - MaxAge: 14, - MaxBackups: 10, - }) - } - - // Builds the address and a listener. - laddr := viper.GetString("Address") + ":" + viper.GetString("Port") - listener, err := net.Listen("tcp", laddr) - if err != nil { - log.Fatal(err) - } - - // Tell the user the port in which is listening. - fmt.Println("Listening on", listener.Addr().String()) - - // Starts the server. - if err := http.Serve(listener, handler()); err != nil { - log.Fatal(err) - } -} - -func handler() http.Handler { - db, err := storm.Open(viper.GetString("Database")) - if err != nil { - log.Fatal(err) - } - - fm := &filemanager.FileManager{ - NoAuth: viper.GetBool("NoAuth"), - BaseURL: viper.GetString("BaseURL"), - PrefixURL: viper.GetString("PrefixURL"), - ReCaptchaKey: viper.GetString("ReCaptchaKey"), - ReCaptchaSecret: viper.GetString("ReCaptchaSecret"), - DefaultUser: &filemanager.User{ - AllowCommands: viper.GetBool("AllowCommands"), - AllowEdit: viper.GetBool("AllowEdit"), - AllowNew: viper.GetBool("AllowNew"), - AllowPublish: viper.GetBool("AllowPublish"), - Commands: viper.GetStringSlice("Commands"), - Rules: []*filemanager.Rule{}, - Locale: viper.GetString("Locale"), - CSS: "", - Scope: viper.GetString("Scope"), - FileSystem: fileutils.Dir(viper.GetString("Scope")), - ViewMode: viper.GetString("ViewMode"), - }, - Store: &filemanager.Store{ - Config: bolt.ConfigStore{DB: db}, - Users: bolt.UsersStore{DB: db}, - Share: bolt.ShareStore{DB: db}, - }, - NewFS: func(scope string) filemanager.FileSystem { - return fileutils.Dir(scope) - }, - } - - err = fm.Setup() - if err != nil { - log.Fatal(err) - } - - switch viper.GetString("StaticGen") { - case "hugo": - hugo := &staticgen.Hugo{ - Root: viper.GetString("Scope"), - Public: filepath.Join(viper.GetString("Scope"), "public"), - Args: []string{}, - CleanPublic: true, - } - - if err = fm.Attach(hugo); err != nil { - log.Fatal(err) - } - case "jekyll": - jekyll := &staticgen.Jekyll{ - Root: viper.GetString("Scope"), - Public: filepath.Join(viper.GetString("Scope"), "_site"), - Args: []string{"build"}, - CleanPublic: true, - } - - if err = fm.Attach(jekyll); err != nil { - log.Fatal(err) - } - } - - return h.Handler(fm) -} +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/asdine/storm" + + lumberjack "gopkg.in/natefinch/lumberjack.v2" + + "github.com/hacdias/filemanager" + "github.com/hacdias/filemanager/bolt" + h "github.com/hacdias/filemanager/http" + "github.com/hacdias/filemanager/staticgen" + "github.com/hacdias/fileutils" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var ( + addr string + config string + database string + scope string + commands string + logfile string + staticg string + locale string + baseurl string + prefixurl string + viewMode string + recaptchakey string + recaptchasecret string + port int + noAuth bool + allowCommands bool + allowEdit bool + allowNew bool + allowPublish bool + showVer bool +) + +func init() { + flag.StringVarP(&config, "config", "c", "", "Configuration file") + flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)") + flag.StringVarP(&addr, "address", "a", "", "Address to listen to (default is all of them)") + flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file") + flag.StringVarP(&logfile, "log", "l", "stdout", "Errors logger; can use 'stdout', 'stderr' or file") + flag.StringVarP(&scope, "scope", "s", ".", "Default scope option for new users") + flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL") + flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users") + flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL") + flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users") + flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key") + flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret") + flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") + flag.BoolVar(&allowEdit, "allow-edit", true, "Default allow edit option for new users") + flag.BoolVar(&allowPublish, "allow-publish", true, "Default allow publish option for new users") + flag.BoolVar(&allowNew, "allow-new", true, "Default allow new option for new users") + flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication") + flag.StringVar(&locale, "locale", "", "Default locale for new users, set it empty to enable auto detect from browser") + flag.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable") + flag.BoolVarP(&showVer, "version", "v", false, "Show version") +} + +func setupViper() { + viper.SetDefault("Address", "") + viper.SetDefault("Port", "0") + viper.SetDefault("Database", "./filemanager.db") + viper.SetDefault("Scope", ".") + viper.SetDefault("Logger", "stdout") + viper.SetDefault("Commands", []string{"git", "svn", "hg"}) + viper.SetDefault("AllowCommmands", true) + viper.SetDefault("AllowEdit", true) + viper.SetDefault("AllowNew", true) + viper.SetDefault("AllowPublish", true) + viper.SetDefault("StaticGen", "") + viper.SetDefault("Locale", "") + viper.SetDefault("NoAuth", false) + viper.SetDefault("BaseURL", "") + viper.SetDefault("PrefixURL", "") + viper.SetDefault("ViewMode", filemanager.MosaicViewMode) + viper.SetDefault("ReCaptchaKey", "") + viper.SetDefault("ReCaptchaSecret", "") + + viper.BindPFlag("Port", flag.Lookup("port")) + viper.BindPFlag("Address", flag.Lookup("address")) + viper.BindPFlag("Database", flag.Lookup("database")) + viper.BindPFlag("Scope", flag.Lookup("scope")) + viper.BindPFlag("Logger", flag.Lookup("log")) + viper.BindPFlag("Commands", flag.Lookup("commands")) + viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands")) + viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit")) + viper.BindPFlag("AlowNew", flag.Lookup("allow-new")) + viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish")) + viper.BindPFlag("Locale", flag.Lookup("locale")) + viper.BindPFlag("StaticGen", flag.Lookup("staticgen")) + viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) + viper.BindPFlag("BaseURL", flag.Lookup("baseurl")) + viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl")) + viper.BindPFlag("ViewMode", flag.Lookup("view-mode")) + viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key")) + viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret")) + + viper.SetConfigName("filemanager") + viper.AddConfigPath(".") +} + +func printVersion() { + fmt.Println("filemanager version", filemanager.Version) + os.Exit(0) +} + +func main() { + setupViper() + flag.Parse() + + if showVer { + printVersion() + } + + // Add a configuration file if set. + if config != "" { + ext := filepath.Ext(config) + dir := filepath.Dir(config) + config = strings.TrimSuffix(config, ext) + + if dir != "" { + viper.AddConfigPath(dir) + config = strings.TrimPrefix(config, dir) + } + + viper.SetConfigName(config) + } + + // Read configuration from a file if exists. + err := viper.ReadInConfig() + if err != nil { + if _, ok := err.(viper.ConfigParseError); ok { + panic(err) + } + } + + // Set up process log before anything bad happens. + switch viper.GetString("Logger") { + case "stdout": + log.SetOutput(os.Stdout) + case "stderr": + log.SetOutput(os.Stderr) + case "": + log.SetOutput(ioutil.Discard) + default: + log.SetOutput(&lumberjack.Logger{ + Filename: logfile, + MaxSize: 100, + MaxAge: 14, + MaxBackups: 10, + }) + } + + // Builds the address and a listener. + laddr := viper.GetString("Address") + ":" + viper.GetString("Port") + listener, err := net.Listen("tcp", laddr) + if err != nil { + log.Fatal(err) + } + + // Tell the user the port in which is listening. + fmt.Println("Listening on", listener.Addr().String()) + + // Starts the server. + if err := http.Serve(listener, handler()); err != nil { + log.Fatal(err) + } +} + +func handler() http.Handler { + db, err := storm.Open(viper.GetString("Database")) + if err != nil { + log.Fatal(err) + } + + fm := &filemanager.FileManager{ + NoAuth: viper.GetBool("NoAuth"), + BaseURL: viper.GetString("BaseURL"), + PrefixURL: viper.GetString("PrefixURL"), + ReCaptchaKey: viper.GetString("ReCaptchaKey"), + ReCaptchaSecret: viper.GetString("ReCaptchaSecret"), + DefaultUser: &filemanager.User{ + AllowCommands: viper.GetBool("AllowCommands"), + AllowEdit: viper.GetBool("AllowEdit"), + AllowNew: viper.GetBool("AllowNew"), + AllowPublish: viper.GetBool("AllowPublish"), + Commands: viper.GetStringSlice("Commands"), + Rules: []*filemanager.Rule{}, + Locale: viper.GetString("Locale"), + CSS: "", + Scope: viper.GetString("Scope"), + FileSystem: fileutils.Dir(viper.GetString("Scope")), + ViewMode: viper.GetString("ViewMode"), + }, + Store: &filemanager.Store{ + Config: bolt.ConfigStore{DB: db}, + Users: bolt.UsersStore{DB: db}, + Share: bolt.ShareStore{DB: db}, + }, + NewFS: func(scope string) filemanager.FileSystem { + return fileutils.Dir(scope) + }, + } + + err = fm.Setup() + if err != nil { + log.Fatal(err) + } + + switch viper.GetString("StaticGen") { + case "hugo": + hugo := &staticgen.Hugo{ + Root: viper.GetString("Scope"), + Public: filepath.Join(viper.GetString("Scope"), "public"), + Args: []string{}, + CleanPublic: true, + } + + if err = fm.Attach(hugo); err != nil { + log.Fatal(err) + } + case "jekyll": + jekyll := &staticgen.Jekyll{ + Root: viper.GetString("Scope"), + Public: filepath.Join(viper.GetString("Scope"), "_site"), + Args: []string{"build"}, + CleanPublic: true, + } + + if err = fm.Attach(jekyll); err != nil { + log.Fatal(err) + } + } + + return h.Handler(fm) +} diff --git a/doc.go b/doc.go index d6cc768f1f..71177fe578 100644 --- a/doc.go +++ b/doc.go @@ -1,73 +1,73 @@ -/* -Package filemanager provides a web interface to access your files -wherever you are. To use this package as a middleware for your app, -you'll need to import both File Manager and File Manager HTTP packages. - - import ( - fm "github.com/hacdias/filemanager" - h "github.com/hacdias/filemanager/http" - ) - -Then, you should create a new FileManager object with your options. In this -case, I'm using BoltDB (via Storm package) as a Store. So, you'll also need -to import "github.com/hacdias/filemanager/bolt". - - db, _ := storm.Open("bolt.db") - - m := &fm.FileManager{ - NoAuth: false, - DefaultUser: &fm.User{ - AllowCommands: true, - AllowEdit: true, - AllowNew: true, - AllowPublish: true, - Commands: []string{"git"}, - Rules: []*fm.Rule{}, - Locale: "en", - CSS: "", - Scope: ".", - FileSystem: fileutils.Dir("."), - }, - Store: &fm.Store{ - Config: bolt.ConfigStore{DB: db}, - Users: bolt.UsersStore{DB: db}, - Share: bolt.ShareStore{DB: db}, - }, - NewFS: func(scope string) fm.FileSystem { - return fileutils.Dir(scope) - }, - } - -The credentials for the first user are always 'admin' for both the user and -the password, and they can be changed later through the settings. The first -user is always an Admin and has all of the permissions set to 'true'. - -Then, you should set the Prefix URL and the Base URL, using the following -functions: - - m.SetBaseURL("/") - m.SetPrefixURL("/") - -The Prefix URL is a part of the path that is already stripped from the -r.URL.Path variable before the request arrives to File Manager's handler. -This is a function that will rarely be used. You can see one example on Caddy -filemanager plugin. - -The Base URL is the URL path where you want File Manager to be available in. If -you want to be available at the root path, you should call: - - m.SetBaseURL("/") - -But if you want to access it at '/admin', you would call: - - m.SetBaseURL("/admin") - -Now, that you already have a File Manager instance created, you just need to -add it to your handlers using m.ServeHTTP which is compatible to http.Handler. -We also have a m.ServeWithErrorsHTTP that returns the status code and an error. - -One simple implementation for this, at port 80, in the root of the domain, would be: - - http.ListenAndServe(":80", h.Handler(m)) -*/ -package filemanager +/* +Package filemanager provides a web interface to access your files +wherever you are. To use this package as a middleware for your app, +you'll need to import both File Manager and File Manager HTTP packages. + + import ( + fm "github.com/hacdias/filemanager" + h "github.com/hacdias/filemanager/http" + ) + +Then, you should create a new FileManager object with your options. In this +case, I'm using BoltDB (via Storm package) as a Store. So, you'll also need +to import "github.com/hacdias/filemanager/bolt". + + db, _ := storm.Open("bolt.db") + + m := &fm.FileManager{ + NoAuth: false, + DefaultUser: &fm.User{ + AllowCommands: true, + AllowEdit: true, + AllowNew: true, + AllowPublish: true, + Commands: []string{"git"}, + Rules: []*fm.Rule{}, + Locale: "en", + CSS: "", + Scope: ".", + FileSystem: fileutils.Dir("."), + }, + Store: &fm.Store{ + Config: bolt.ConfigStore{DB: db}, + Users: bolt.UsersStore{DB: db}, + Share: bolt.ShareStore{DB: db}, + }, + NewFS: func(scope string) fm.FileSystem { + return fileutils.Dir(scope) + }, + } + +The credentials for the first user are always 'admin' for both the user and +the password, and they can be changed later through the settings. The first +user is always an Admin and has all of the permissions set to 'true'. + +Then, you should set the Prefix URL and the Base URL, using the following +functions: + + m.SetBaseURL("/") + m.SetPrefixURL("/") + +The Prefix URL is a part of the path that is already stripped from the +r.URL.Path variable before the request arrives to File Manager's handler. +This is a function that will rarely be used. You can see one example on Caddy +filemanager plugin. + +The Base URL is the URL path where you want File Manager to be available in. If +you want to be available at the root path, you should call: + + m.SetBaseURL("/") + +But if you want to access it at '/admin', you would call: + + m.SetBaseURL("/admin") + +Now, that you already have a File Manager instance created, you just need to +add it to your handlers using m.ServeHTTP which is compatible to http.Handler. +We also have a m.ServeWithErrorsHTTP that returns the status code and an error. + +One simple implementation for this, at port 80, in the root of the domain, would be: + + http.ListenAndServe(":80", h.Handler(m)) +*/ +package filemanager diff --git a/http/http.go b/http/http.go index 1aaa6a74ba..6431fdb2de 100644 --- a/http/http.go +++ b/http/http.go @@ -1,344 +1,344 @@ -package http - -import ( - "encoding/json" - "html/template" - "log" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - fm "github.com/hacdias/filemanager" -) - -// Handler returns a function compatible with http.HandleFunc. -func Handler(m *fm.FileManager) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - code, err := serve(&fm.Context{ - FileManager: m, - User: nil, - File: nil, - }, w, r) - - if code >= 400 { - w.WriteHeader(code) - - txt := http.StatusText(code) - log.Printf("%v: %v %v\n", r.URL.Path, code, txt) - w.Write([]byte(txt + "\n")) - } - - if err != nil { - log.Print(err) - } - }) -} - -// serve is the main entry point of this HTML application. -func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Checks if the URL contains the baseURL and strips it. Otherwise, it just - // returns a 404 fm.Error because we're not supposed to be here! - p := strings.TrimPrefix(r.URL.Path, c.BaseURL) - - if len(p) >= len(r.URL.Path) && c.BaseURL != "" { - return http.StatusNotFound, nil - } - - r.URL.Path = p - - // Check if this request is made to the service worker. If so, - // pass it through a template to add the needed variables. - if r.URL.Path == "/sw.js" { - return renderFile(c, w, "sw.js") - } - - // Checks if this request is made to the static assets folder. If so, and - // if it is a GET request, returns with the asset. Otherwise, returns - // a status not implemented. - if matchURL(r.URL.Path, "/static") { - if r.Method != http.MethodGet { - return http.StatusNotImplemented, nil - } - - return staticHandler(c, w, r) - } - - // Checks if this request is made to the API and directs to the - // API handler if so. - if matchURL(r.URL.Path, "/api") { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") - return apiHandler(c, w, r) - } - - // If it is a request to the preview and a static website generator is - // active, build the preview. - if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview") - return c.StaticGen.Preview(c, w, r) - } - - if strings.HasPrefix(r.URL.Path, "/share/") { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/") - return sharePage(c, w, r) - } - - // Any other request should show the index.html file. - w.Header().Set("x-frame-options", "SAMEORIGIN") - w.Header().Set("x-content-type", "nosniff") - w.Header().Set("x-xss-protection", "1; mode=block") - - return renderFile(c, w, "index.html") -} - -// staticHandler handles the static assets path. -func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - if r.URL.Path != "/static/manifest.json" { - http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r) - return 0, nil - } - - return renderFile(c, w, "static/manifest.json") -} - -// apiHandler is the main entry point for the /api endpoint. -func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - if r.URL.Path == "/auth/get" { - return authHandler(c, w, r) - } - - if r.URL.Path == "/auth/renew" { - return renewAuthHandler(c, w, r) - } - - valid, _ := validateAuth(c, r) - if !valid { - return http.StatusForbidden, nil - } - - c.Router, r.URL.Path = splitURL(r.URL.Path) - - if !c.User.Allowed(r.URL.Path) { - return http.StatusForbidden, nil - } - - if c.StaticGen != nil { - // If we are using the 'magic url' for the settings, - // we should redirect the request for the acutual path. - if r.URL.Path == "/settings" { - r.URL.Path = c.StaticGen.SettingsPath() - } - - // Executes the Static website generator hook. - code, err := c.StaticGen.Hook(c, w, r) - if code != 0 || err != nil { - return code, err - } - } - - if c.Router == "checksum" || c.Router == "download" { - var err error - c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User) - if err != nil { - return ErrorToHTTP(err, false), err - } - } - - var code int - var err error - - switch c.Router { - case "download": - code, err = downloadHandler(c, w, r) - case "checksum": - code, err = checksumHandler(c, w, r) - case "command": - code, err = command(c, w, r) - case "search": - code, err = search(c, w, r) - case "resource": - code, err = resourceHandler(c, w, r) - case "users": - code, err = usersHandler(c, w, r) - case "settings": - code, err = settingsHandler(c, w, r) - case "share": - code, err = shareHandler(c, w, r) - default: - code = http.StatusNotFound - } - - return code, err -} - -// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512. -func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - query := r.URL.Query().Get("algo") - - val, err := c.File.Checksum(query) - if err == fm.ErrInvalidOption { - return http.StatusBadRequest, err - } else if err != nil { - return http.StatusInternalServerError, err - } - - w.Write([]byte(val)) - return 0, nil -} - -// splitURL splits the path and returns everything that stands -// before the first slash and everything that goes after. -func splitURL(path string) (string, string) { - if path == "" { - return "", "" - } - - path = strings.TrimPrefix(path, "/") - - i := strings.Index(path, "/") - if i == -1 { - return "", path - } - - return path[0:i], path[i:] -} - -// renderFile renders a file using a template with some needed variables. -func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) { - tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file))) - - var contentType string - switch filepath.Ext(file) { - case ".html": - contentType = "text/html" - case ".js": - contentType = "application/javascript" - case ".json": - contentType = "application/json" - default: - contentType = "text" - } - - w.Header().Set("Content-Type", contentType+"; charset=utf-8") - - data := map[string]interface{}{ - "BaseURL": c.RootURL(), - "NoAuth": c.NoAuth, - "Version": fm.Version, - "CSS": template.CSS(c.CSS), - "ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "", - "ReCaptchaKey": c.ReCaptchaKey, - "ReCaptchaSecret": c.ReCaptchaSecret, - } - - if c.StaticGen != nil { - data["StaticGen"] = c.StaticGen.Name() - } - - err := tpl.Execute(w, data) - - if err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil -} - -// sharePage build the share page. -func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - s, err := c.Store.Share.Get(r.URL.Path) - if err == fm.ErrNotExist { - w.WriteHeader(http.StatusNotFound) - return renderFile(c, w, "static/share/404.html") - } - - if err != nil { - return http.StatusInternalServerError, err - } - - if s.Expires && s.ExpireDate.Before(time.Now()) { - c.Store.Share.Delete(s.Hash) - w.WriteHeader(http.StatusNotFound) - return renderFile(c, w, "static/share/404.html") - } - - r.URL.Path = s.Path - - info, err := os.Stat(s.Path) - if err != nil { - c.Store.Share.Delete(s.Hash) - return ErrorToHTTP(err, false), err - } - - c.File = &fm.File{ - Path: s.Path, - Name: info.Name(), - ModTime: info.ModTime(), - Mode: info.Mode(), - IsDir: info.IsDir(), - Size: info.Size(), - } - - dl := r.URL.Query().Get("dl") - - if dl == "" || dl == "0" { - tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html"))) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - - err := tpl.Execute(w, map[string]interface{}{ - "BaseURL": c.RootURL(), - "File": c.File, - }) - - if err != nil { - return http.StatusInternalServerError, err - } - return 0, nil - } - - return downloadHandler(c, w, r) -} - -// renderJSON prints the JSON version of data to the browser. -func renderJSON(w http.ResponseWriter, data interface{}) (int, error) { - marsh, err := json.Marshal(data) - if err != nil { - return http.StatusInternalServerError, err - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if _, err := w.Write(marsh); err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil -} - -// matchURL checks if the first URL matches the second. -func matchURL(first, second string) bool { - first = strings.ToLower(first) - second = strings.ToLower(second) - - return strings.HasPrefix(first, second) -} - -// ErrorToHTTP converts errors to HTTP Status Code. -func ErrorToHTTP(err error, gone bool) int { - switch { - case err == nil: - return http.StatusOK - case os.IsPermission(err): - return http.StatusForbidden - case os.IsNotExist(err): - if !gone { - return http.StatusNotFound - } - - return http.StatusGone - case os.IsExist(err): - return http.StatusConflict - default: - return http.StatusInternalServerError - } -} +package http + +import ( + "encoding/json" + "html/template" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + fm "github.com/hacdias/filemanager" +) + +// Handler returns a function compatible with http.HandleFunc. +func Handler(m *fm.FileManager) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + code, err := serve(&fm.Context{ + FileManager: m, + User: nil, + File: nil, + }, w, r) + + if code >= 400 { + w.WriteHeader(code) + + txt := http.StatusText(code) + log.Printf("%v: %v %v\n", r.URL.Path, code, txt) + w.Write([]byte(txt + "\n")) + } + + if err != nil { + log.Print(err) + } + }) +} + +// serve is the main entry point of this HTML application. +func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Checks if the URL contains the baseURL and strips it. Otherwise, it just + // returns a 404 fm.Error because we're not supposed to be here! + p := strings.TrimPrefix(r.URL.Path, c.BaseURL) + + if len(p) >= len(r.URL.Path) && c.BaseURL != "" { + return http.StatusNotFound, nil + } + + r.URL.Path = p + + // Check if this request is made to the service worker. If so, + // pass it through a template to add the needed variables. + if r.URL.Path == "/sw.js" { + return renderFile(c, w, "sw.js") + } + + // Checks if this request is made to the static assets folder. If so, and + // if it is a GET request, returns with the asset. Otherwise, returns + // a status not implemented. + if matchURL(r.URL.Path, "/static") { + if r.Method != http.MethodGet { + return http.StatusNotImplemented, nil + } + + return staticHandler(c, w, r) + } + + // Checks if this request is made to the API and directs to the + // API handler if so. + if matchURL(r.URL.Path, "/api") { + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") + return apiHandler(c, w, r) + } + + // If it is a request to the preview and a static website generator is + // active, build the preview. + if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil { + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview") + return c.StaticGen.Preview(c, w, r) + } + + if strings.HasPrefix(r.URL.Path, "/share/") { + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/") + return sharePage(c, w, r) + } + + // Any other request should show the index.html file. + w.Header().Set("x-frame-options", "SAMEORIGIN") + w.Header().Set("x-content-type", "nosniff") + w.Header().Set("x-xss-protection", "1; mode=block") + + return renderFile(c, w, "index.html") +} + +// staticHandler handles the static assets path. +func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + if r.URL.Path != "/static/manifest.json" { + http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r) + return 0, nil + } + + return renderFile(c, w, "static/manifest.json") +} + +// apiHandler is the main entry point for the /api endpoint. +func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + if r.URL.Path == "/auth/get" { + return authHandler(c, w, r) + } + + if r.URL.Path == "/auth/renew" { + return renewAuthHandler(c, w, r) + } + + valid, _ := validateAuth(c, r) + if !valid { + return http.StatusForbidden, nil + } + + c.Router, r.URL.Path = splitURL(r.URL.Path) + + if !c.User.Allowed(r.URL.Path) { + return http.StatusForbidden, nil + } + + if c.StaticGen != nil { + // If we are using the 'magic url' for the settings, + // we should redirect the request for the acutual path. + if r.URL.Path == "/settings" { + r.URL.Path = c.StaticGen.SettingsPath() + } + + // Executes the Static website generator hook. + code, err := c.StaticGen.Hook(c, w, r) + if code != 0 || err != nil { + return code, err + } + } + + if c.Router == "checksum" || c.Router == "download" { + var err error + c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User) + if err != nil { + return ErrorToHTTP(err, false), err + } + } + + var code int + var err error + + switch c.Router { + case "download": + code, err = downloadHandler(c, w, r) + case "checksum": + code, err = checksumHandler(c, w, r) + case "command": + code, err = command(c, w, r) + case "search": + code, err = search(c, w, r) + case "resource": + code, err = resourceHandler(c, w, r) + case "users": + code, err = usersHandler(c, w, r) + case "settings": + code, err = settingsHandler(c, w, r) + case "share": + code, err = shareHandler(c, w, r) + default: + code = http.StatusNotFound + } + + return code, err +} + +// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512. +func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + query := r.URL.Query().Get("algo") + + val, err := c.File.Checksum(query) + if err == fm.ErrInvalidOption { + return http.StatusBadRequest, err + } else if err != nil { + return http.StatusInternalServerError, err + } + + w.Write([]byte(val)) + return 0, nil +} + +// splitURL splits the path and returns everything that stands +// before the first slash and everything that goes after. +func splitURL(path string) (string, string) { + if path == "" { + return "", "" + } + + path = strings.TrimPrefix(path, "/") + + i := strings.Index(path, "/") + if i == -1 { + return "", path + } + + return path[0:i], path[i:] +} + +// renderFile renders a file using a template with some needed variables. +func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) { + tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file))) + + var contentType string + switch filepath.Ext(file) { + case ".html": + contentType = "text/html" + case ".js": + contentType = "application/javascript" + case ".json": + contentType = "application/json" + default: + contentType = "text" + } + + w.Header().Set("Content-Type", contentType+"; charset=utf-8") + + data := map[string]interface{}{ + "BaseURL": c.RootURL(), + "NoAuth": c.NoAuth, + "Version": fm.Version, + "CSS": template.CSS(c.CSS), + "ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "", + "ReCaptchaKey": c.ReCaptchaKey, + "ReCaptchaSecret": c.ReCaptchaSecret, + } + + if c.StaticGen != nil { + data["StaticGen"] = c.StaticGen.Name() + } + + err := tpl.Execute(w, data) + + if err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} + +// sharePage build the share page. +func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + s, err := c.Store.Share.Get(r.URL.Path) + if err == fm.ErrNotExist { + w.WriteHeader(http.StatusNotFound) + return renderFile(c, w, "static/share/404.html") + } + + if err != nil { + return http.StatusInternalServerError, err + } + + if s.Expires && s.ExpireDate.Before(time.Now()) { + c.Store.Share.Delete(s.Hash) + w.WriteHeader(http.StatusNotFound) + return renderFile(c, w, "static/share/404.html") + } + + r.URL.Path = s.Path + + info, err := os.Stat(s.Path) + if err != nil { + c.Store.Share.Delete(s.Hash) + return ErrorToHTTP(err, false), err + } + + c.File = &fm.File{ + Path: s.Path, + Name: info.Name(), + ModTime: info.ModTime(), + Mode: info.Mode(), + IsDir: info.IsDir(), + Size: info.Size(), + } + + dl := r.URL.Query().Get("dl") + + if dl == "" || dl == "0" { + tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html"))) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + err := tpl.Execute(w, map[string]interface{}{ + "BaseURL": c.RootURL(), + "File": c.File, + }) + + if err != nil { + return http.StatusInternalServerError, err + } + return 0, nil + } + + return downloadHandler(c, w, r) +} + +// renderJSON prints the JSON version of data to the browser. +func renderJSON(w http.ResponseWriter, data interface{}) (int, error) { + marsh, err := json.Marshal(data) + if err != nil { + return http.StatusInternalServerError, err + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, err := w.Write(marsh); err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} + +// matchURL checks if the first URL matches the second. +func matchURL(first, second string) bool { + first = strings.ToLower(first) + second = strings.ToLower(second) + + return strings.HasPrefix(first, second) +} + +// ErrorToHTTP converts errors to HTTP Status Code. +func ErrorToHTTP(err error, gone bool) int { + switch { + case err == nil: + return http.StatusOK + case os.IsPermission(err): + return http.StatusForbidden + case os.IsNotExist(err): + if !gone { + return http.StatusNotFound + } + + return http.StatusGone + case os.IsExist(err): + return http.StatusConflict + default: + return http.StatusInternalServerError + } +} diff --git a/http/resource.go b/http/resource.go index a312de90ae..011329936a 100644 --- a/http/resource.go +++ b/http/resource.go @@ -1,386 +1,386 @@ -package http - -import ( - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "time" - - fm "github.com/hacdias/filemanager" - "github.com/hacdias/fileutils" -) - -// sanitizeURL sanitizes the URL to prevent path transversal -// using fileutils.SlashClean and adds the trailing slash bar. -func sanitizeURL(url string) string { - path := fileutils.SlashClean(url) - if strings.HasSuffix(url, "/") && path != "/" { - return path + "/" - } - return path -} - -func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - r.URL.Path = sanitizeURL(r.URL.Path) - - switch r.Method { - case http.MethodGet: - return resourceGetHandler(c, w, r) - case http.MethodDelete: - return resourceDeleteHandler(c, w, r) - case http.MethodPut: - // Before save command handler. - path := filepath.Join(c.User.Scope, r.URL.Path) - if err := c.Runner("before_save", path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - code, err := resourcePostPutHandler(c, w, r) - if code != http.StatusOK { - return code, err - } - - // After save command handler. - if err := c.Runner("after_save", path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - return code, err - case http.MethodPatch: - return resourcePatchHandler(c, w, r) - case http.MethodPost: - return resourcePostPutHandler(c, w, r) - } - - return http.StatusNotImplemented, nil -} - -func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Gets the information of the directory/file. - f, err := fm.GetInfo(r.URL, c.FileManager, c.User) - if err != nil { - return ErrorToHTTP(err, false), err - } - - // If it's a dir and the path doesn't end with a trailing slash, - // add a trailing slash to the path. - if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") { - r.URL.Path = r.URL.Path + "/" - } - - // If it is a dir, go and serve the listing. - if f.IsDir { - c.File = f - return listingHandler(c, w, r) - } - - // Tries to get the file type. - if err = f.GetFileType(true); err != nil { - return ErrorToHTTP(err, true), err - } - - // Serve a preview if the file can't be edited or the - // user has no permission to edit this file. Otherwise, - // just serve the editor. - if !f.CanBeEdited() || !c.User.AllowEdit { - f.Kind = "preview" - return renderJSON(w, f) - } - - f.Kind = "editor" - - // Tries to get the editor data. - if err = f.GetEditor(); err != nil { - return http.StatusInternalServerError, err - } - - return renderJSON(w, f) -} - -func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - f := c.File - f.Kind = "listing" - - // Tries to get the listing data. - if err := f.GetListing(c.User, r); err != nil { - return ErrorToHTTP(err, true), err - } - - listing := f.Listing - - // Defines the cookie scope. - cookieScope := c.RootURL() - if cookieScope == "" { - cookieScope = "/" - } - - // Copy the query values into the Listing struct - if sort, order, err := handleSortOrder(w, r, cookieScope); err == nil { - listing.Sort = sort - listing.Order = order - } else { - return http.StatusBadRequest, err - } - - listing.ApplySort() - return renderJSON(w, f) -} - -func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Prevent the removal of the root directory. - if r.URL.Path == "/" || !c.User.AllowEdit { - return http.StatusForbidden, nil - } - - // Fire the before trigger. - if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - // Remove the file or folder. - err := c.User.FileSystem.RemoveAll(r.URL.Path) - if err != nil { - return ErrorToHTTP(err, true), err - } - - // Fire the after trigger. - if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil -} - -func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - if !c.User.AllowNew && r.Method == http.MethodPost { - return http.StatusForbidden, nil - } - - if !c.User.AllowEdit && r.Method == http.MethodPut { - return http.StatusForbidden, nil - } - - // Discard any invalid upload before returning to avoid connection - // reset error. - defer func() { - io.Copy(ioutil.Discard, r.Body) - }() - - // Checks if the current request is for a directory and not a file. - if strings.HasSuffix(r.URL.Path, "/") { - // If the method is PUT, we return 405 Method not Allowed, because - // POST should be used instead. - if r.Method == http.MethodPut { - return http.StatusMethodNotAllowed, nil - } - - // Otherwise we try to create the directory. - err := c.User.FileSystem.Mkdir(r.URL.Path, 0776) - return ErrorToHTTP(err, false), err - } - - // If using POST method, we are trying to create a new file so it is not - // desirable to override an already existent file. Thus, we check - // if the file already exists. If so, we just return a 409 Conflict. - if r.Method == http.MethodPost && r.Header.Get("Action") != "override" { - if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil { - return http.StatusConflict, errors.New("There is already a file on that path") - } - } - - // Fire the before trigger. - if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - // Create/Open the file. - f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) - if err != nil { - return ErrorToHTTP(err, false), err - } - defer f.Close() - - // Copies the new content for the file. - _, err = io.Copy(f, r.Body) - if err != nil { - return ErrorToHTTP(err, false), err - } - - // Gets the info about the file. - fi, err := f.Stat() - if err != nil { - return ErrorToHTTP(err, false), err - } - - // Check if this instance has a Static Generator and handles publishing - // or scheduling if it's the case. - if c.StaticGen != nil { - code, err := resourcePublishSchedule(c, w, r) - if code != 0 { - return code, err - } - } - - // Writes the ETag Header. - etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()) - w.Header().Set("ETag", etag) - - // Fire the after trigger. - if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - return http.StatusOK, nil -} - -func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - publish := r.Header.Get("Publish") - schedule := r.Header.Get("Schedule") - - if publish != "true" && schedule == "" { - return 0, nil - } - - if !c.User.AllowPublish { - return http.StatusForbidden, nil - } - - if publish == "true" { - return resourcePublish(c, w, r) - } - - t, err := time.Parse("2006-01-02T15:04", schedule) - if err != nil { - return http.StatusInternalServerError, err - } - - c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { - _, err := resourcePublish(c, w, r) - if err != nil { - log.Print(err) - } - }) - - return http.StatusOK, nil -} - -func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - path := filepath.Join(c.User.Scope, r.URL.Path) - - // Before save command handler. - if err := c.Runner("before_publish", path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - code, err := c.StaticGen.Publish(c, w, r) - if err != nil { - return code, err - } - - // Executed the before publish command. - if err := c.Runner("before_publish", path, "", c.User); err != nil { - return http.StatusInternalServerError, err - } - - return code, nil -} - -// resourcePatchHandler is the entry point for resource handler. -func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - if !c.User.AllowEdit { - return http.StatusForbidden, nil - } - - dst := r.Header.Get("Destination") - action := r.Header.Get("Action") - dst, err := url.QueryUnescape(dst) - if err != nil { - return ErrorToHTTP(err, true), err - } - - src := r.URL.Path - - if dst == "/" || src == "/" { - return http.StatusForbidden, nil - } - - if action == "copy" { - // Fire the after trigger. - if err := c.Runner("before_copy", src, dst, c.User); err != nil { - return http.StatusInternalServerError, err - } - - // Copy the file. - err = c.User.FileSystem.Copy(src, dst) - - // Fire the after trigger. - if err := c.Runner("after_copy", src, dst, c.User); err != nil { - return http.StatusInternalServerError, err - } - } else { - // Fire the after trigger. - if err := c.Runner("before_rename", src, dst, c.User); err != nil { - return http.StatusInternalServerError, err - } - - // Rename the file. - err = c.User.FileSystem.Rename(src, dst) - - // Fire the after trigger. - if err := c.Runner("after_rename", src, dst, c.User); err != nil { - return http.StatusInternalServerError, err - } - } - - return ErrorToHTTP(err, true), err -} - -// handleSortOrder gets and stores for a Listing the 'sort' and 'order', -// and reads 'limit' if given. The latter is 0 if not given. Sets cookies. -func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) { - sort = r.URL.Query().Get("sort") - order = r.URL.Query().Get("order") - - // If the query 'sort' or 'order' is empty, use defaults or any values - // previously saved in Cookies. - switch sort { - case "": - sort = "name" - if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { - sort = sortCookie.Value - } - case "name", "size": - http.SetCookie(w, &http.Cookie{ - Name: "sort", - Value: sort, - MaxAge: 31536000, - Path: scope, - Secure: r.TLS != nil, - }) - } - - switch order { - case "": - order = "asc" - if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { - order = orderCookie.Value - } - case "asc", "desc": - http.SetCookie(w, &http.Cookie{ - Name: "order", - Value: order, - MaxAge: 31536000, - Path: scope, - Secure: r.TLS != nil, - }) - } - - return -} +package http + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + fm "github.com/hacdias/filemanager" + "github.com/hacdias/fileutils" +) + +// sanitizeURL sanitizes the URL to prevent path transversal +// using fileutils.SlashClean and adds the trailing slash bar. +func sanitizeURL(url string) string { + path := fileutils.SlashClean(url) + if strings.HasSuffix(url, "/") && path != "/" { + return path + "/" + } + return path +} + +func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + r.URL.Path = sanitizeURL(r.URL.Path) + + switch r.Method { + case http.MethodGet: + return resourceGetHandler(c, w, r) + case http.MethodDelete: + return resourceDeleteHandler(c, w, r) + case http.MethodPut: + // Before save command handler. + path := filepath.Join(c.User.Scope, r.URL.Path) + if err := c.Runner("before_save", path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + code, err := resourcePostPutHandler(c, w, r) + if code != http.StatusOK { + return code, err + } + + // After save command handler. + if err := c.Runner("after_save", path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + return code, err + case http.MethodPatch: + return resourcePatchHandler(c, w, r) + case http.MethodPost: + return resourcePostPutHandler(c, w, r) + } + + return http.StatusNotImplemented, nil +} + +func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Gets the information of the directory/file. + f, err := fm.GetInfo(r.URL, c.FileManager, c.User) + if err != nil { + return ErrorToHTTP(err, false), err + } + + // If it's a dir and the path doesn't end with a trailing slash, + // add a trailing slash to the path. + if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") { + r.URL.Path = r.URL.Path + "/" + } + + // If it is a dir, go and serve the listing. + if f.IsDir { + c.File = f + return listingHandler(c, w, r) + } + + // Tries to get the file type. + if err = f.GetFileType(true); err != nil { + return ErrorToHTTP(err, true), err + } + + // Serve a preview if the file can't be edited or the + // user has no permission to edit this file. Otherwise, + // just serve the editor. + if !f.CanBeEdited() || !c.User.AllowEdit { + f.Kind = "preview" + return renderJSON(w, f) + } + + f.Kind = "editor" + + // Tries to get the editor data. + if err = f.GetEditor(); err != nil { + return http.StatusInternalServerError, err + } + + return renderJSON(w, f) +} + +func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + f := c.File + f.Kind = "listing" + + // Tries to get the listing data. + if err := f.GetListing(c.User, r); err != nil { + return ErrorToHTTP(err, true), err + } + + listing := f.Listing + + // Defines the cookie scope. + cookieScope := c.RootURL() + if cookieScope == "" { + cookieScope = "/" + } + + // Copy the query values into the Listing struct + if sort, order, err := handleSortOrder(w, r, cookieScope); err == nil { + listing.Sort = sort + listing.Order = order + } else { + return http.StatusBadRequest, err + } + + listing.ApplySort() + return renderJSON(w, f) +} + +func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Prevent the removal of the root directory. + if r.URL.Path == "/" || !c.User.AllowEdit { + return http.StatusForbidden, nil + } + + // Fire the before trigger. + if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + // Remove the file or folder. + err := c.User.FileSystem.RemoveAll(r.URL.Path) + if err != nil { + return ErrorToHTTP(err, true), err + } + + // Fire the after trigger. + if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil +} + +func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + if !c.User.AllowNew && r.Method == http.MethodPost { + return http.StatusForbidden, nil + } + + if !c.User.AllowEdit && r.Method == http.MethodPut { + return http.StatusForbidden, nil + } + + // Discard any invalid upload before returning to avoid connection + // reset error. + defer func() { + io.Copy(ioutil.Discard, r.Body) + }() + + // Checks if the current request is for a directory and not a file. + if strings.HasSuffix(r.URL.Path, "/") { + // If the method is PUT, we return 405 Method not Allowed, because + // POST should be used instead. + if r.Method == http.MethodPut { + return http.StatusMethodNotAllowed, nil + } + + // Otherwise we try to create the directory. + err := c.User.FileSystem.Mkdir(r.URL.Path, 0776) + return ErrorToHTTP(err, false), err + } + + // If using POST method, we are trying to create a new file so it is not + // desirable to override an already existent file. Thus, we check + // if the file already exists. If so, we just return a 409 Conflict. + if r.Method == http.MethodPost && r.Header.Get("Action") != "override" { + if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil { + return http.StatusConflict, errors.New("There is already a file on that path") + } + } + + // Fire the before trigger. + if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + // Create/Open the file. + f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) + if err != nil { + return ErrorToHTTP(err, false), err + } + defer f.Close() + + // Copies the new content for the file. + _, err = io.Copy(f, r.Body) + if err != nil { + return ErrorToHTTP(err, false), err + } + + // Gets the info about the file. + fi, err := f.Stat() + if err != nil { + return ErrorToHTTP(err, false), err + } + + // Check if this instance has a Static Generator and handles publishing + // or scheduling if it's the case. + if c.StaticGen != nil { + code, err := resourcePublishSchedule(c, w, r) + if code != 0 { + return code, err + } + } + + // Writes the ETag Header. + etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()) + w.Header().Set("ETag", etag) + + // Fire the after trigger. + if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil +} + +func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + publish := r.Header.Get("Publish") + schedule := r.Header.Get("Schedule") + + if publish != "true" && schedule == "" { + return 0, nil + } + + if !c.User.AllowPublish { + return http.StatusForbidden, nil + } + + if publish == "true" { + return resourcePublish(c, w, r) + } + + t, err := time.Parse("2006-01-02T15:04", schedule) + if err != nil { + return http.StatusInternalServerError, err + } + + c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { + _, err := resourcePublish(c, w, r) + if err != nil { + log.Print(err) + } + }) + + return http.StatusOK, nil +} + +func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + path := filepath.Join(c.User.Scope, r.URL.Path) + + // Before save command handler. + if err := c.Runner("before_publish", path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + code, err := c.StaticGen.Publish(c, w, r) + if err != nil { + return code, err + } + + // Executed the before publish command. + if err := c.Runner("before_publish", path, "", c.User); err != nil { + return http.StatusInternalServerError, err + } + + return code, nil +} + +// resourcePatchHandler is the entry point for resource handler. +func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + if !c.User.AllowEdit { + return http.StatusForbidden, nil + } + + dst := r.Header.Get("Destination") + action := r.Header.Get("Action") + dst, err := url.QueryUnescape(dst) + if err != nil { + return ErrorToHTTP(err, true), err + } + + src := r.URL.Path + + if dst == "/" || src == "/" { + return http.StatusForbidden, nil + } + + if action == "copy" { + // Fire the after trigger. + if err := c.Runner("before_copy", src, dst, c.User); err != nil { + return http.StatusInternalServerError, err + } + + // Copy the file. + err = c.User.FileSystem.Copy(src, dst) + + // Fire the after trigger. + if err := c.Runner("after_copy", src, dst, c.User); err != nil { + return http.StatusInternalServerError, err + } + } else { + // Fire the after trigger. + if err := c.Runner("before_rename", src, dst, c.User); err != nil { + return http.StatusInternalServerError, err + } + + // Rename the file. + err = c.User.FileSystem.Rename(src, dst) + + // Fire the after trigger. + if err := c.Runner("after_rename", src, dst, c.User); err != nil { + return http.StatusInternalServerError, err + } + } + + return ErrorToHTTP(err, true), err +} + +// handleSortOrder gets and stores for a Listing the 'sort' and 'order', +// and reads 'limit' if given. The latter is 0 if not given. Sets cookies. +func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) { + sort = r.URL.Query().Get("sort") + order = r.URL.Query().Get("order") + + // If the query 'sort' or 'order' is empty, use defaults or any values + // previously saved in Cookies. + switch sort { + case "": + sort = "name" + if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { + sort = sortCookie.Value + } + case "name", "size": + http.SetCookie(w, &http.Cookie{ + Name: "sort", + Value: sort, + MaxAge: 31536000, + Path: scope, + Secure: r.TLS != nil, + }) + } + + switch order { + case "": + order = "asc" + if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { + order = orderCookie.Value + } + case "asc", "desc": + http.SetCookie(w, &http.Cookie{ + Name: "order", + Value: order, + MaxAge: 31536000, + Path: scope, + Secure: r.TLS != nil, + }) + } + + return +} diff --git a/http/websockets.go b/http/websockets.go index 8f6d867f3e..8d73ab061e 100644 --- a/http/websockets.go +++ b/http/websockets.go @@ -1,339 +1,339 @@ -package http - -import ( - "bytes" - "encoding/json" - "mime" - "net/http" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/gorilla/websocket" - fm "github.com/hacdias/filemanager" -) - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -var ( - cmdNotImplemented = []byte("Command not implemented.") - cmdNotAllowed = []byte("Command not allowed.") -) - -// command handles the requests for VCS related commands: git, svn and mercurial -func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Upgrades the connection to a websocket and checks for fm.Errors. - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return 0, err - } - defer conn.Close() - - var ( - message []byte - command []string - ) - - // Starts an infinite loop until a valid command is captured. - for { - _, message, err = conn.ReadMessage() - if err != nil { - return http.StatusInternalServerError, err - } - - command = strings.Split(string(message), " ") - if len(command) != 0 { - break - } - } - - // Check if the command is allowed - allowed := false - - for _, cmd := range c.User.Commands { - if cmd == command[0] { - allowed = true - } - } - - if !allowed { - err = conn.WriteMessage(websocket.BinaryMessage, cmdNotAllowed) - if err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil - } - - // Check if the program is talled is installed on the computer. - if _, err = exec.LookPath(command[0]); err != nil { - err = conn.WriteMessage(websocket.BinaryMessage, cmdNotImplemented) - if err != nil { - return http.StatusInternalServerError, err - } - - return http.StatusNotImplemented, nil - } - - // Gets the path and initializes a buffer. - path := c.User.Scope + "/" + r.URL.Path - path = filepath.Clean(path) - buff := new(bytes.Buffer) - - // Sets up the command executation. - cmd := exec.Command(command[0], command[1:]...) - cmd.Dir = path - cmd.Stderr = buff - cmd.Stdout = buff - - // Starts the command and checks for fm.Errors. - err = cmd.Start() - if err != nil { - return http.StatusInternalServerError, err - } - - // Set a 'done' variable to check whetever the command has already finished - // running or not. This verification is done using a goroutine that uses the - // method .Wait() from the command. - done := false - go func() { - err = cmd.Wait() - done = true - }() - - // Function to print the current information on the buffer to the connection. - print := func() error { - by := buff.Bytes() - if len(by) > 0 { - err = conn.WriteMessage(websocket.TextMessage, by) - if err != nil { - return err - } - } - - return nil - } - - // While the command hasn't finished running, continue sending the output - // to the client in intervals of 100 milliseconds. - for !done { - if err = print(); err != nil { - return http.StatusInternalServerError, err - } - - time.Sleep(100 * time.Millisecond) - } - - // After the command is done executing, send the output one more time to the - // browser to make sure it gets the latest information. - if err = print(); err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil -} - -var ( - typeRegexp = regexp.MustCompile(`type:(\w+)`) -) - -type condition func(path string) bool - -type searchOptions struct { - CaseInsensitive bool - Conditions []condition - Terms []string -} - -func extensionCondition(extension string) condition { - return func(path string) bool { - return filepath.Ext(path) == "."+extension - } -} - -func imageCondition(path string) bool { - extension := filepath.Ext(path) - mimetype := mime.TypeByExtension(extension) - - return strings.HasPrefix(mimetype, "image") -} - -func audioCondition(path string) bool { - extension := filepath.Ext(path) - mimetype := mime.TypeByExtension(extension) - - return strings.HasPrefix(mimetype, "audio") -} - -func videoCondition(path string) bool { - extension := filepath.Ext(path) - mimetype := mime.TypeByExtension(extension) - - return strings.HasPrefix(mimetype, "video") -} - -func parseSearch(value string) *searchOptions { - opts := &searchOptions{ - CaseInsensitive: strings.Contains(value, "case:insensitive"), - Conditions: []condition{}, - Terms: []string{}, - } - - // removes the options from the value - value = strings.Replace(value, "case:insensitive", "", -1) - value = strings.Replace(value, "case:sensitive", "", -1) - value = strings.TrimSpace(value) - - types := typeRegexp.FindAllStringSubmatch(value, -1) - for _, t := range types { - if len(t) == 1 { - continue - } - - switch t[1] { - case "image": - opts.Conditions = append(opts.Conditions, imageCondition) - case "audio", "music": - opts.Conditions = append(opts.Conditions, audioCondition) - case "video": - opts.Conditions = append(opts.Conditions, videoCondition) - default: - opts.Conditions = append(opts.Conditions, extensionCondition(t[1])) - } - } - - if len(types) > 0 { - // Remove the fields from the search value. - value = typeRegexp.ReplaceAllString(value, "") - } - - // If it's canse insensitive, put everything in lowercase. - if opts.CaseInsensitive { - value = strings.ToLower(value) - } - - // Remove the spaces from the search value. - value = strings.TrimSpace(value) - - if value == "" { - return opts - } - - // if the value starts with " and finishes what that character, we will - // only search for that term - if value[0] == '"' && value[len(value)-1] == '"' { - unique := strings.TrimPrefix(value, "\"") - unique = strings.TrimSuffix(unique, "\"") - - opts.Terms = []string{unique} - return opts - } - - opts.Terms = strings.Split(value, " ") - return opts -} - -// search searches for a file or directory. -func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Upgrades the connection to a websocket and checks for fm.Errors. - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return 0, err - } - defer conn.Close() - - var ( - value string - search *searchOptions - message []byte - ) - - // Starts an infinite loop until a valid command is captured. - for { - _, message, err = conn.ReadMessage() - if err != nil { - return http.StatusInternalServerError, err - } - - if len(message) != 0 { - value = string(message) - break - } - } - - search = parseSearch(value) - scope := strings.TrimPrefix(r.URL.Path, "/") - scope = "/" + scope - scope = c.User.Scope + scope - scope = strings.Replace(scope, "\\", "/", -1) - scope = filepath.Clean(scope) - - err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error { - if search.CaseInsensitive { - path = strings.ToLower(path) - } - - path = strings.TrimPrefix(path, scope) - path = strings.TrimPrefix(path, "/") - path = strings.Replace(path, "\\", "/", -1) - - // Only execute if there are conditions to meet. - if len(search.Conditions) > 0 { - match := false - - for _, t := range search.Conditions { - if t(path) { - match = true - break - } - } - - // If doesn't meet the condition, go to the next. - if !match { - return nil - } - } - - if len(search.Terms) > 0 { - is := false - - // Checks if matches the terms and if it is allowed. - for _, term := range search.Terms { - if is { - break - } - - if strings.Contains(path, term) { - if !c.User.Allowed(path) { - return nil - } - - is = true - } - } - - if !is { - return nil - } - } - - response, _ := json.Marshal(map[string]interface{}{ - "dir": f.IsDir(), - "path": path, - }) - - return conn.WriteMessage(websocket.TextMessage, response) - }) - - if err != nil { - return http.StatusInternalServerError, err - } - - return 0, nil -} +package http + +import ( + "bytes" + "encoding/json" + "mime" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/gorilla/websocket" + fm "github.com/hacdias/filemanager" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +var ( + cmdNotImplemented = []byte("Command not implemented.") + cmdNotAllowed = []byte("Command not allowed.") +) + +// command handles the requests for VCS related commands: git, svn and mercurial +func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Upgrades the connection to a websocket and checks for fm.Errors. + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return 0, err + } + defer conn.Close() + + var ( + message []byte + command []string + ) + + // Starts an infinite loop until a valid command is captured. + for { + _, message, err = conn.ReadMessage() + if err != nil { + return http.StatusInternalServerError, err + } + + command = strings.Split(string(message), " ") + if len(command) != 0 { + break + } + } + + // Check if the command is allowed + allowed := false + + for _, cmd := range c.User.Commands { + if cmd == command[0] { + allowed = true + } + } + + if !allowed { + err = conn.WriteMessage(websocket.BinaryMessage, cmdNotAllowed) + if err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil + } + + // Check if the program is talled is installed on the computer. + if _, err = exec.LookPath(command[0]); err != nil { + err = conn.WriteMessage(websocket.BinaryMessage, cmdNotImplemented) + if err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusNotImplemented, nil + } + + // Gets the path and initializes a buffer. + path := c.User.Scope + "/" + r.URL.Path + path = filepath.Clean(path) + buff := new(bytes.Buffer) + + // Sets up the command executation. + cmd := exec.Command(command[0], command[1:]...) + cmd.Dir = path + cmd.Stderr = buff + cmd.Stdout = buff + + // Starts the command and checks for fm.Errors. + err = cmd.Start() + if err != nil { + return http.StatusInternalServerError, err + } + + // Set a 'done' variable to check whetever the command has already finished + // running or not. This verification is done using a goroutine that uses the + // method .Wait() from the command. + done := false + go func() { + err = cmd.Wait() + done = true + }() + + // Function to print the current information on the buffer to the connection. + print := func() error { + by := buff.Bytes() + if len(by) > 0 { + err = conn.WriteMessage(websocket.TextMessage, by) + if err != nil { + return err + } + } + + return nil + } + + // While the command hasn't finished running, continue sending the output + // to the client in intervals of 100 milliseconds. + for !done { + if err = print(); err != nil { + return http.StatusInternalServerError, err + } + + time.Sleep(100 * time.Millisecond) + } + + // After the command is done executing, send the output one more time to the + // browser to make sure it gets the latest information. + if err = print(); err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} + +var ( + typeRegexp = regexp.MustCompile(`type:(\w+)`) +) + +type condition func(path string) bool + +type searchOptions struct { + CaseInsensitive bool + Conditions []condition + Terms []string +} + +func extensionCondition(extension string) condition { + return func(path string) bool { + return filepath.Ext(path) == "."+extension + } +} + +func imageCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "image") +} + +func audioCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "audio") +} + +func videoCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "video") +} + +func parseSearch(value string) *searchOptions { + opts := &searchOptions{ + CaseInsensitive: strings.Contains(value, "case:insensitive"), + Conditions: []condition{}, + Terms: []string{}, + } + + // removes the options from the value + value = strings.Replace(value, "case:insensitive", "", -1) + value = strings.Replace(value, "case:sensitive", "", -1) + value = strings.TrimSpace(value) + + types := typeRegexp.FindAllStringSubmatch(value, -1) + for _, t := range types { + if len(t) == 1 { + continue + } + + switch t[1] { + case "image": + opts.Conditions = append(opts.Conditions, imageCondition) + case "audio", "music": + opts.Conditions = append(opts.Conditions, audioCondition) + case "video": + opts.Conditions = append(opts.Conditions, videoCondition) + default: + opts.Conditions = append(opts.Conditions, extensionCondition(t[1])) + } + } + + if len(types) > 0 { + // Remove the fields from the search value. + value = typeRegexp.ReplaceAllString(value, "") + } + + // If it's canse insensitive, put everything in lowercase. + if opts.CaseInsensitive { + value = strings.ToLower(value) + } + + // Remove the spaces from the search value. + value = strings.TrimSpace(value) + + if value == "" { + return opts + } + + // if the value starts with " and finishes what that character, we will + // only search for that term + if value[0] == '"' && value[len(value)-1] == '"' { + unique := strings.TrimPrefix(value, "\"") + unique = strings.TrimSuffix(unique, "\"") + + opts.Terms = []string{unique} + return opts + } + + opts.Terms = strings.Split(value, " ") + return opts +} + +// search searches for a file or directory. +func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Upgrades the connection to a websocket and checks for fm.Errors. + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return 0, err + } + defer conn.Close() + + var ( + value string + search *searchOptions + message []byte + ) + + // Starts an infinite loop until a valid command is captured. + for { + _, message, err = conn.ReadMessage() + if err != nil { + return http.StatusInternalServerError, err + } + + if len(message) != 0 { + value = string(message) + break + } + } + + search = parseSearch(value) + scope := strings.TrimPrefix(r.URL.Path, "/") + scope = "/" + scope + scope = c.User.Scope + scope + scope = strings.Replace(scope, "\\", "/", -1) + scope = filepath.Clean(scope) + + err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error { + if search.CaseInsensitive { + path = strings.ToLower(path) + } + + path = strings.TrimPrefix(path, scope) + path = strings.TrimPrefix(path, "/") + path = strings.Replace(path, "\\", "/", -1) + + // Only execute if there are conditions to meet. + if len(search.Conditions) > 0 { + match := false + + for _, t := range search.Conditions { + if t(path) { + match = true + break + } + } + + // If doesn't meet the condition, go to the next. + if !match { + return nil + } + } + + if len(search.Terms) > 0 { + is := false + + // Checks if matches the terms and if it is allowed. + for _, term := range search.Terms { + if is { + break + } + + if strings.Contains(path, term) { + if !c.User.Allowed(path) { + return nil + } + + is = true + } + } + + if !is { + return nil + } + } + + response, _ := json.Marshal(map[string]interface{}{ + "dir": f.IsDir(), + "path": path, + }) + + return conn.WriteMessage(websocket.TextMessage, response) + }) + + if err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} diff --git a/rice-box.go.REMOVED.git-id b/rice-box.go.REMOVED.git-id deleted file mode 100644 index 47eb68e898..0000000000 --- a/rice-box.go.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -882c59547968e4fb9a65aa8d1c838409f198e51b \ No newline at end of file diff --git a/staticgen/hugo.go b/staticgen/hugo.go index 4a12ea7720..bde72754a9 100644 --- a/staticgen/hugo.go +++ b/staticgen/hugo.go @@ -1,194 +1,194 @@ -package staticgen - -import ( - "errors" - "io/ioutil" - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - - fm "github.com/hacdias/filemanager" - "github.com/hacdias/varutils" -) - -var ( - errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") -) - -// Hugo is the Hugo static website generator. -type Hugo struct { - // Website root - Root string `name:"Website Root"` - // Public folder - Public string `name:"Public Directory"` - // Hugo executable path - Exe string `name:"Hugo Executable"` - // Hugo arguments - Args []string `name:"Hugo Arguments"` - // Indicates if we should clean public before a new publish. - CleanPublic bool `name:"Clean Public"` - // previewPath is the temporary path for a preview - previewPath string -} - -// SettingsPath retrieves the correct settings path. -func (h Hugo) SettingsPath() string { - var frontmatter string - var err error - - if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil { - frontmatter = "yaml" - } - - if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil { - frontmatter = "json" - } - - if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil { - frontmatter = "toml" - } - - if frontmatter == "" { - return "/settings" - } - - return "/config." + frontmatter -} - -// Name is the plugin's name. -func (h Hugo) Name() string { - return "hugo" -} - -// Hook is the pre-api handler. -func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // If we are not using HTTP Post, we shall return Method Not Allowed - // since we are only working with this method. - if r.Method != http.MethodPost { - return 0, nil - } - - if c.Router != "resource" { - return 0, nil - } - - // We only care about creating new files from archetypes here. So... - if r.Header.Get("Archetype") == "" { - return 0, nil - } - - if !c.User.AllowNew { - return http.StatusForbidden, nil - } - - filename := filepath.Join(c.User.Scope, r.URL.Path) - archetype := r.Header.Get("archetype") - - ext := filepath.Ext(filename) - - // If the request isn't for a markdown file, we can't - // handle it. - if ext != ".markdown" && ext != ".md" { - return http.StatusBadRequest, errUnsupportedFileType - } - - // Tries to create a new file based on this archetype. - args := []string{"new", filename, "--kind", archetype} - if err := runCommand(h.Exe, args, h.Root); err != nil { - return http.StatusInternalServerError, err - } - - // Writes the location of the new file to the Header. - w.Header().Set("Location", "/files/content/"+filename) - return http.StatusCreated, nil -} - -// Publish publishes a post. -func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - filename := filepath.Join(c.User.Scope, r.URL.Path) - - // We only run undraft command if it is a file. - if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") { - if err := h.undraft(filename); err != nil { - return http.StatusInternalServerError, err - } - } - - // Regenerates the file - h.run(false) - - return 0, nil -} - -// Preview handles the preview path. -func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Get a new temporary path if there is none. - if h.previewPath == "" { - path, err := ioutil.TempDir("", "") - if err != nil { - return http.StatusInternalServerError, err - } - - h.previewPath = path - } - - // Build the arguments to execute Hugo: change the base URL, - // build the drafts and update the destination. - args := h.Args - args = append(args, "--baseURL", c.RootURL()+"/preview/") - args = append(args, "--buildDrafts") - args = append(args, "--destination", h.previewPath) - - // Builds the preview. - if err := runCommand(h.Exe, args, h.Root); err != nil { - return http.StatusInternalServerError, err - } - - // Serves the temporary path with the preview. - http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r) - return 0, nil -} - -func (h Hugo) run(force bool) { - // If the CleanPublic option is enabled, clean it. - if h.CleanPublic { - os.RemoveAll(h.Public) - } - - // Prevent running if watching is enabled - if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force { - if len(h.Args) > pos && h.Args[pos+1] != "false" { - return - } - - if len(h.Args) == pos+1 { - return - } - } - - if err := runCommand(h.Exe, h.Args, h.Root); err != nil { - log.Println(err) - } -} - -func (h Hugo) undraft(file string) error { - args := []string{"undraft", file} - if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") { - return err - } - - return nil -} - -// Setup sets up the plugin. -func (h *Hugo) Setup() error { - var err error - if h.Exe, err = exec.LookPath("hugo"); err != nil { - return err - } - - return nil -} +package staticgen + +import ( + "errors" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + + fm "github.com/hacdias/filemanager" + "github.com/hacdias/varutils" +) + +var ( + errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") +) + +// Hugo is the Hugo static website generator. +type Hugo struct { + // Website root + Root string `name:"Website Root"` + // Public folder + Public string `name:"Public Directory"` + // Hugo executable path + Exe string `name:"Hugo Executable"` + // Hugo arguments + Args []string `name:"Hugo Arguments"` + // Indicates if we should clean public before a new publish. + CleanPublic bool `name:"Clean Public"` + // previewPath is the temporary path for a preview + previewPath string +} + +// SettingsPath retrieves the correct settings path. +func (h Hugo) SettingsPath() string { + var frontmatter string + var err error + + if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil { + frontmatter = "yaml" + } + + if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil { + frontmatter = "json" + } + + if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil { + frontmatter = "toml" + } + + if frontmatter == "" { + return "/settings" + } + + return "/config." + frontmatter +} + +// Name is the plugin's name. +func (h Hugo) Name() string { + return "hugo" +} + +// Hook is the pre-api handler. +func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // If we are not using HTTP Post, we shall return Method Not Allowed + // since we are only working with this method. + if r.Method != http.MethodPost { + return 0, nil + } + + if c.Router != "resource" { + return 0, nil + } + + // We only care about creating new files from archetypes here. So... + if r.Header.Get("Archetype") == "" { + return 0, nil + } + + if !c.User.AllowNew { + return http.StatusForbidden, nil + } + + filename := filepath.Join(c.User.Scope, r.URL.Path) + archetype := r.Header.Get("archetype") + + ext := filepath.Ext(filename) + + // If the request isn't for a markdown file, we can't + // handle it. + if ext != ".markdown" && ext != ".md" { + return http.StatusBadRequest, errUnsupportedFileType + } + + // Tries to create a new file based on this archetype. + args := []string{"new", filename, "--kind", archetype} + if err := runCommand(h.Exe, args, h.Root); err != nil { + return http.StatusInternalServerError, err + } + + // Writes the location of the new file to the Header. + w.Header().Set("Location", "/files/content/"+filename) + return http.StatusCreated, nil +} + +// Publish publishes a post. +func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + filename := filepath.Join(c.User.Scope, r.URL.Path) + + // We only run undraft command if it is a file. + if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") { + if err := h.undraft(filename); err != nil { + return http.StatusInternalServerError, err + } + } + + // Regenerates the file + h.run(false) + + return 0, nil +} + +// Preview handles the preview path. +func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Get a new temporary path if there is none. + if h.previewPath == "" { + path, err := ioutil.TempDir("", "") + if err != nil { + return http.StatusInternalServerError, err + } + + h.previewPath = path + } + + // Build the arguments to execute Hugo: change the base URL, + // build the drafts and update the destination. + args := h.Args + args = append(args, "--baseURL", c.RootURL()+"/preview/") + args = append(args, "--buildDrafts") + args = append(args, "--destination", h.previewPath) + + // Builds the preview. + if err := runCommand(h.Exe, args, h.Root); err != nil { + return http.StatusInternalServerError, err + } + + // Serves the temporary path with the preview. + http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r) + return 0, nil +} + +func (h Hugo) run(force bool) { + // If the CleanPublic option is enabled, clean it. + if h.CleanPublic { + os.RemoveAll(h.Public) + } + + // Prevent running if watching is enabled + if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force { + if len(h.Args) > pos && h.Args[pos+1] != "false" { + return + } + + if len(h.Args) == pos+1 { + return + } + } + + if err := runCommand(h.Exe, h.Args, h.Root); err != nil { + log.Println(err) + } +} + +func (h Hugo) undraft(file string) error { + args := []string{"undraft", file} + if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") { + return err + } + + return nil +} + +// Setup sets up the plugin. +func (h *Hugo) Setup() error { + var err error + if h.Exe, err = exec.LookPath("hugo"); err != nil { + return err + } + + return nil +} diff --git a/staticgen/jekyll.go b/staticgen/jekyll.go index fea4dd1a61..308ac1ddf5 100644 --- a/staticgen/jekyll.go +++ b/staticgen/jekyll.go @@ -1,125 +1,125 @@ -package staticgen - -import ( - "io/ioutil" - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - - fm "github.com/hacdias/filemanager" -) - -// Jekyll is the Jekyll static website generator. -type Jekyll struct { - // Website root - Root string `name:"Website Root"` - // Public folder - Public string `name:"Public Directory"` - // Jekyll executable path - Exe string `name:"Executable"` - // Jekyll arguments - Args []string `name:"Arguments"` - // Indicates if we should clean public before a new publish. - CleanPublic bool `name:"Clean Public"` - // previewPath is the temporary path for a preview - previewPath string -} - -// Name is the plugin's name. -func (j Jekyll) Name() string { - return "jekyll" -} - -// SettingsPath retrieves the correct settings path. -func (j Jekyll) SettingsPath() string { - return "/_config.yml" -} - -// Hook is the pre-api handler. -func (j Jekyll) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - return 0, nil -} - -// Publish publishes a post. -func (j Jekyll) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - filename := filepath.Join(c.User.Scope, r.URL.Path) - - // We only run undraft command if it is a file. - if err := j.undraft(filename); err != nil { - return http.StatusInternalServerError, err - } - - // Regenerates the file - j.run() - - return 0, nil -} - -// Preview handles the preview path. -func (j *Jekyll) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Get a new temporary path if there is none. - if j.previewPath == "" { - path, err := ioutil.TempDir("", "") - if err != nil { - return http.StatusInternalServerError, err - } - - j.previewPath = path - } - - // Build the arguments to execute Hugo: change the base URL, - // build the drafts and update the destination. - args := j.Args - args = append(args, "--baseurl", c.RootURL()+"/preview/") - args = append(args, "--drafts") - args = append(args, "--destination", j.previewPath) - - // Builds the preview. - if err := runCommand(j.Exe, args, j.Root); err != nil { - return http.StatusInternalServerError, err - } - - // Serves the temporary path with the preview. - http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r) - return 0, nil -} - -func (j Jekyll) run() { - // If the CleanPublic option is enabled, clean it. - if j.CleanPublic { - os.RemoveAll(j.Public) - } - - if err := runCommand(j.Exe, j.Args, j.Root); err != nil { - log.Println(err) - } -} - -func (j Jekyll) undraft(file string) error { - if !strings.Contains(file, "_drafts") { - return nil - } - - return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1)) -} - -// Setup sets up the plugin. -func (j *Jekyll) Setup() error { - var err error - if j.Exe, err = exec.LookPath("jekyll"); err != nil { - return err - } - - if len(j.Args) == 0 { - j.Args = []string{"build"} - } - - if j.Args[0] != "build" { - j.Args = append([]string{"build"}, j.Args...) - } - - return nil -} +package staticgen + +import ( + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + + fm "github.com/hacdias/filemanager" +) + +// Jekyll is the Jekyll static website generator. +type Jekyll struct { + // Website root + Root string `name:"Website Root"` + // Public folder + Public string `name:"Public Directory"` + // Jekyll executable path + Exe string `name:"Executable"` + // Jekyll arguments + Args []string `name:"Arguments"` + // Indicates if we should clean public before a new publish. + CleanPublic bool `name:"Clean Public"` + // previewPath is the temporary path for a preview + previewPath string +} + +// Name is the plugin's name. +func (j Jekyll) Name() string { + return "jekyll" +} + +// SettingsPath retrieves the correct settings path. +func (j Jekyll) SettingsPath() string { + return "/_config.yml" +} + +// Hook is the pre-api handler. +func (j Jekyll) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + return 0, nil +} + +// Publish publishes a post. +func (j Jekyll) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + filename := filepath.Join(c.User.Scope, r.URL.Path) + + // We only run undraft command if it is a file. + if err := j.undraft(filename); err != nil { + return http.StatusInternalServerError, err + } + + // Regenerates the file + j.run() + + return 0, nil +} + +// Preview handles the preview path. +func (j *Jekyll) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { + // Get a new temporary path if there is none. + if j.previewPath == "" { + path, err := ioutil.TempDir("", "") + if err != nil { + return http.StatusInternalServerError, err + } + + j.previewPath = path + } + + // Build the arguments to execute Hugo: change the base URL, + // build the drafts and update the destination. + args := j.Args + args = append(args, "--baseurl", c.RootURL()+"/preview/") + args = append(args, "--drafts") + args = append(args, "--destination", j.previewPath) + + // Builds the preview. + if err := runCommand(j.Exe, args, j.Root); err != nil { + return http.StatusInternalServerError, err + } + + // Serves the temporary path with the preview. + http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r) + return 0, nil +} + +func (j Jekyll) run() { + // If the CleanPublic option is enabled, clean it. + if j.CleanPublic { + os.RemoveAll(j.Public) + } + + if err := runCommand(j.Exe, j.Args, j.Root); err != nil { + log.Println(err) + } +} + +func (j Jekyll) undraft(file string) error { + if !strings.Contains(file, "_drafts") { + return nil + } + + return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1)) +} + +// Setup sets up the plugin. +func (j *Jekyll) Setup() error { + var err error + if j.Exe, err = exec.LookPath("jekyll"); err != nil { + return err + } + + if len(j.Args) == 0 { + j.Args = []string{"build"} + } + + if j.Args[0] != "build" { + j.Args = append([]string{"build"}, j.Args...) + } + + return nil +} diff --git a/staticgen/staticgen.go b/staticgen/staticgen.go index ef862223c1..64f3d69474 100644 --- a/staticgen/staticgen.go +++ b/staticgen/staticgen.go @@ -1,19 +1,19 @@ -package staticgen - -import ( - "errors" - "os/exec" -) - -// runCommand executes an external command -func runCommand(command string, args []string, path string) error { - cmd := exec.Command(command, args...) - cmd.Dir = path - out, err := cmd.CombinedOutput() - - if err != nil { - return errors.New(string(out)) - } - - return nil -} +package staticgen + +import ( + "errors" + "os/exec" +) + +// runCommand executes an external command +func runCommand(command string, args []string, path string) error { + cmd := exec.Command(command, args...) + cmd.Dir = path + out, err := cmd.CombinedOutput() + + if err != nil { + return errors.New(string(out)) + } + + return nil +}