Former-commit-id: 0d8742754bb756ad3a83599850dae5f477282430 [formerly 5cb7d75b695d8400fc2af87edd551d6450e7365f] [formerly a6a814c40a5ff4f195c4ab470d4fccc92bd8c1c8 [formerly 99c8c92c6c6d1225380dbbfc5b61d4263a129156]]
Former-commit-id: 45eba5ff05f8e64fbf33d9d670e19a0cf4880656 [formerly 88dc856045b9d51596f36ce387b1c4f3e85a7d3c]
Former-commit-id: 1eadaef460060da8ae71df3c66f242c844992725
This commit is contained in:
Henrique Dias 2017-10-30 15:24:06 +00:00
parent 95d43a344c
commit 3d54b2bd90
66 changed files with 5105 additions and 5094 deletions

View File

@ -1,13 +1,13 @@
{ {
"presets": [ "presets": [
["env", { "modules": false }], ["env", { "modules": false }],
"stage-2" "stage-2"
], ],
"plugins": ["transform-runtime"], "plugins": ["transform-runtime"],
"env": { "env": {
"test": { "test": {
"presets": ["env", "stage-2"], "presets": ["env", "stage-2"],
"plugins": [ "istanbul" ] "plugins": [ "istanbul" ]
} }
} }
} }

View File

@ -1,4 +1,4 @@
assets/ assets/
testdata/ testdata/
caddy/ caddy/
.github/ .github/

View File

@ -1,14 +1,14 @@
root = true root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
# 4 space indentation # 4 space indentation
[*.go] [*.go]
indent_style = tab indent_style = tab
indent_size = 4 indent_size = 4

View File

@ -1,2 +1,2 @@
build/*.js build/*.js
config/*.js config/*.js

View File

@ -1,27 +1,27 @@
// http://eslint.org/docs/user-guide/configuring // http://eslint.org/docs/user-guide/configuring
module.exports = { module.exports = {
root: true, root: true,
parser: 'babel-eslint', parser: 'babel-eslint',
parserOptions: { parserOptions: {
sourceType: 'module' sourceType: 'module'
}, },
env: { env: {
browser: true, browser: true,
}, },
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: 'standard', extends: 'standard',
// required to lint *.vue files // required to lint *.vue files
plugins: [ plugins: [
'html' 'html'
], ],
// add your custom rules here // add your custom rules here
'rules': { 'rules': {
// allow paren-less arrow functions // allow paren-less arrow functions
'arrow-parens': 0, 'arrow-parens': 0,
// allow async-await // allow async-await
'generator-star-spacing': 0, 'generator-star-spacing': 0,
// allow debugger during development // allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
} }
} }

View File

@ -1,24 +1,24 @@
### Instructions (remove before submitting): ### 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. 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. 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. 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. 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? ### 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? ### 2. What is your entire Caddyfile?
```text ```text
(Put Caddyfile here) (Put Caddyfile here)
``` ```
### 3. What are you trying to do? ### 3. What are you trying to do?
### 4. What did you expect to see? ### 4. What did you expect to see?
### 5. What did you see instead (give full error messages and/or log)? ### 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? ### 6. How can someone who is starting from scratch reproduce this behaviour as minimally as possible?

View File

@ -1,29 +1,29 @@
build: build:
main: cmd/filemanager/main.go main: cmd/filemanager/main.go
binary: filemanager binary: filemanager
goos: goos:
- darwin - darwin
- linux - linux
- windows - windows
- freebsd - freebsd
- netbsd - netbsd
- openbsd - openbsd
goarch: goarch:
- amd64 - amd64
- 386 - 386
- arm - arm
- arm64 - arm64
ignore: ignore:
- goos: openbsd - goos: openbsd
goarch: arm goarch: arm
goarm: 6 goarm: 6
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
goarm: 6 goarm: 6
archive: archive:
name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}" name_template: "{{.Os}}-{{.Arch}}-{{ .ProjectName }}"
format: tar.gz format: tar.gz
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip format: zip

View File

@ -1,46 +1,46 @@
# Contributor Covenant Code of Conduct # Contributor Covenant Code of Conduct
## Our Pledge ## 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. 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 ## Our Standards
Examples of behavior that contributes to creating a positive environment include: Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language * Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences * Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism * Gracefully accepting constructive criticism
* Focusing on what is best for the community * Focusing on what is best for the community
* Showing empathy towards other community members * Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances * The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks * Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment * Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission * 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 * Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities ## 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 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. 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 ## 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. 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 ## 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. 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. 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 ## 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] 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 [homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/ [version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,14 +1,14 @@
# Contributing # 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: 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` 1. `go get github.com/hacdias/filemanager`
2. `cd $GOPATH/src/github.com/hacdias/filemanager` 2. `cd $GOPATH/src/github.com/hacdias/filemanager`
3. `npm install` 3. `npm install`
4. `npm run dev` - regenerates the static assets automatically 4. `npm run dev` - regenerates the static assets automatically
5. `go install github.com/hacdias/filemanager/cmd/filemanager` 5. `go install github.com/hacdias/filemanager/cmd/filemanager`
6. Execute `$GOPATH/bin/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. 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`. 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`.

View File

@ -1,22 +1,22 @@
FROM golang:alpine FROM golang:alpine
COPY . /go/src/github.com/hacdias/filemanager COPY . /go/src/github.com/hacdias/filemanager
WORKDIR /go/src/github.com/hacdias/filemanager WORKDIR /go/src/github.com/hacdias/filemanager
RUN apk add --no-cache git RUN apk add --no-cache git
RUN go get ./... RUN go get ./...
WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager WORKDIR /go/src/github.com/hacdias/filemanager/cmd/filemanager
RUN CGO_ENABLED=0 go build -a RUN CGO_ENABLED=0 go build -a
RUN mv filemanager /go/bin/filemanager RUN mv filemanager /go/bin/filemanager
FROM scratch FROM scratch
COPY --from=0 /go/bin/filemanager /filemanager COPY --from=0 /go/bin/filemanager /filemanager
VOLUME /srv VOLUME /srv
EXPOSE 80 EXPOSE 80
COPY Docker.json /config.json COPY Docker.json /config.json
ENTRYPOINT ["/filemanager"] ENTRYPOINT ["/filemanager"]
CMD ["--config", "/config.json"] CMD ["--config", "/config.json"]

View File

@ -1,201 +1,201 @@
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions. 1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, "License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document. and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by "Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License. the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all "Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition, control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the "control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity. outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity "You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License. exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, "Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation including but not limited to software source code, documentation
source, and configuration files. source, and configuration files.
"Object" form shall mean any form resulting from mechanical "Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation, not limited to compiled object code, generated documentation,
and conversions to other media types. and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or "Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work copyright notice that is included in or attached to the work
(an example is provided in the Appendix below). (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object "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 form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of, separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof. the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including "Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner 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 or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted" the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution." designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity "Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work. subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of 2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual, this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of, copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form. Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of 3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual, this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made, (except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work, use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s) Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate granted to You under this License for that Work shall terminate
as of the date such litigation is filed. as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the 4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You modifications, and in Source or Object form, provided that You
meet the following conditions: meet the following conditions:
(a) You must give any other recipients of the Work or (a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices (b) You must cause any modified files to carry prominent notices
stating that You changed the files; and stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works (c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work, attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of excluding those notices that do not pertain to any part of
the Derivative Works; and the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its (d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or, documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed that such additional attribution notices cannot be construed
as modifying the License. as modifying the License.
You may add Your own copyright statement to Your modifications and You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use, for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License. the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, 5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions. this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions. with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade 6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor, names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file. origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or 7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS, Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License. risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, 8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise, whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the 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 (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages. has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing 9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer, the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity, and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify, of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability. of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work. APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}" boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright {yyyy} {name of copyright owner} Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.

156
README.md
View File

@ -1,78 +1,78 @@
![Preview](https://user-images.githubusercontent.com/5447088/28537288-39be4288-70a2-11e7-8ce9-0813d59f46b7.gif) ![Preview](https://user-images.githubusercontent.com/5447088/28537288-39be4288-70a2-11e7-8ce9-0813d59f46b7.gif)
# filemanager # filemanager
[![Build](https://img.shields.io/travis/hacdias/filemanager.svg?style=flat-square)](https://travis-ci.org/hacdias/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) [![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) [![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. 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 # Table of contents
+ [Getting started](#getting-started) + [Getting started](#getting-started)
+ [Features](#features) + [Features](#features)
- [Users](#users) - [Users](#users)
- [Search](#search) - [Search](#search)
+ [Contributing](#contributing) + [Contributing](#contributing)
+ [Donate](#donate) + [Donate](#donate)
# Getting started # Getting started
You can find the Getting Started guide on the [documentation](https://henriquedias.com/filemanager/quick-start/). You can find the Getting Started guide on the [documentation](https://henriquedias.com/filemanager/quick-start/).
# Features # Features
Easy login system. Easy login system.
![Login Page](https://user-images.githubusercontent.com/5447088/28432382-975493dc-6d7f-11e7-9190-23f8037159dc.jpg) ![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*. 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) ![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. 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) ![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. 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) ![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. We also allow the users to search in the directories and execute commands if allowed.
## Users ## 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!). 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) ![Users](https://user-images.githubusercontent.com/5447088/28432386-977f388a-6d7f-11e7-9006-87d16f05f1f8.jpg)
## Search ## Search
FileManager allows you to search through your files and it has some options. By default, your search will be something like this: 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 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: 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" "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. 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: 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 this are keywords case:insensitive
``` ```
# Contributing # Contributing
The contributing guidelines can be found [here](https://github.com/hacdias/filemanager/blob/master/CONTRIBUTING.md). The contributing guidelines can be found [here](https://github.com/hacdias/filemanager/blob/master/CONTRIBUTING.md).
# Donate # Donate
Enjoying this project? You can [donate to its creator](https://henriquedias.com/donate/). He will appreciate. Enjoying this project? You can [donate to its creator](https://henriquedias.com/donate/). He will appreciate.

View File

@ -1,31 +1,31 @@
require('./check-versions')() require('./check-versions')()
process.env.NODE_ENV = 'production' process.env.NODE_ENV = 'production'
var ora = require('ora') var ora = require('ora')
var rm = require('rimraf') var rm = require('rimraf')
var path = require('path') var path = require('path')
var chalk = require('chalk') var chalk = require('chalk')
var webpack = require('webpack') var webpack = require('webpack')
var config = require('./config') var config = require('./config')
var webpackConfig = require('./webpack.prod.conf') var webpackConfig = require('./webpack.prod.conf')
var spinner = ora('building for production...') var spinner = ora('building for production...')
spinner.start() spinner.start()
rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => { rm(path.join(config.assetsRoot, config.assetsSubDirectory), err => {
if (err) throw err if (err) throw err
webpack(webpackConfig, function (err, stats) { webpack(webpackConfig, function (err, stats) {
spinner.stop() spinner.stop()
if (err) throw err if (err) throw err
process.stdout.write(stats.toString({ process.stdout.write(stats.toString({
colors: true, colors: true,
modules: false, modules: false,
children: false, children: false,
chunks: false, chunks: false,
chunkModules: false chunkModules: false
}) + '\n\n') }) + '\n\n')
console.log(chalk.cyan(' Build complete.\n')) console.log(chalk.cyan(' Build complete.\n'))
}) })
}) })

View File

@ -1,48 +1,48 @@
var chalk = require('chalk') var chalk = require('chalk')
var semver = require('semver') var semver = require('semver')
var packageConfig = require('../../package.json') var packageConfig = require('../../package.json')
var shell = require('shelljs') var shell = require('shelljs')
function exec (cmd) { function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim() return require('child_process').execSync(cmd).toString().trim()
} }
var versionRequirements = [ var versionRequirements = [
{ {
name: 'node', name: 'node',
currentVersion: semver.clean(process.version), currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node versionRequirement: packageConfig.engines.node
} }
] ]
if (shell.which('npm')) { if (shell.which('npm')) {
versionRequirements.push({ versionRequirements.push({
name: 'npm', name: 'npm',
currentVersion: exec('npm --version'), currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm versionRequirement: packageConfig.engines.npm
}) })
} }
module.exports = function () { module.exports = function () {
var warnings = [] var warnings = []
for (var i = 0; i < versionRequirements.length; i++) { for (var i = 0; i < versionRequirements.length; i++) {
var mod = versionRequirements[i] var mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' + warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' + chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement) chalk.green(mod.versionRequirement)
) )
} }
} }
if (warnings.length) { if (warnings.length) {
console.log('') console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:')) console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log() console.log()
for (var i = 0; i < warnings.length; i++) { for (var i = 0; i < warnings.length; i++) {
var warning = warnings[i] var warning = warnings[i]
console.log(' ' + warning) console.log(' ' + warning)
} }
console.log() console.log()
process.exit(1) process.exit(1)
} }
} }

View File

@ -1,26 +1,26 @@
// see http://vuejs-templates.github.io/webpack for documentation. // see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path') var path = require('path')
module.exports = { module.exports = {
index: path.resolve(__dirname, '../dist/index.html'), index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'), assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static', assetsSubDirectory: 'static',
assetsPublicPath: '{{ .BaseURL }}/', assetsPublicPath: '{{ .BaseURL }}/',
build: { build: {
env: { env: {
NODE_ENV: '"production"' NODE_ENV: '"production"'
}, },
productionSourceMap: true, productionSourceMap: true,
// Run the build command with an extra argument to // Run the build command with an extra argument to
// View the bundle analyzer report after build finishes: // View the bundle analyzer report after build finishes:
// `npm run build --report` // `npm run build --report`
// Set to `true` or `false` to always turn it on or off // Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report bundleAnalyzerReport: process.env.npm_config_report
}, },
dev: { dev: {
env: { env: {
NODE_ENV: '"development"' NODE_ENV: '"development"'
}, },
produceSourceMap: true produceSourceMap: true
} }
} }

View File

@ -1,17 +1,17 @@
// This service worker file is effectively a 'no-op' that will reset any // This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination. // previous service worker registered for the same host:port combination.
// In the production build, this file is replaced with an actual service worker // In the production build, this file is replaced with an actual service worker
// file that will precache your site's local assets. // file that will precache your site's local assets.
// See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 // See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432
self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => { self.addEventListener('activate', () => {
self.clients.matchAll({ type: 'window' }).then(windowClients => { self.clients.matchAll({ type: 'window' }).then(windowClients => {
for (let windowClient of windowClients) { for (let windowClient of windowClients) {
// Force open pages to refresh, so that they have a chance to load the // Force open pages to refresh, so that they have a chance to load the
// fresh navigation response from the local dev server. // fresh navigation response from the local dev server.
windowClient.navigate(windowClient.url); windowClient.navigate(windowClient.url);
} }
}); });
}); });

View File

@ -1,55 +1,55 @@
(function() { (function() {
'use strict'; 'use strict';
// Check to make sure service workers are supported in the current browser, // 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 // and that the current page is accessed from a secure origin. Using a
// service worker from an insecure origin will trigger JS console errors. // service worker from an insecure origin will trigger JS console errors.
const isLocalhost = Boolean(window.location.hostname === 'localhost' || const isLocalhost = Boolean(window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' || window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4. // 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match( window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
) )
); );
window.addEventListener('load', function() { window.addEventListener('load', function() {
if ('serviceWorker' in navigator && if ('serviceWorker' in navigator &&
(window.location.protocol === 'https:' || isLocalhost)) { (window.location.protocol === 'https:' || isLocalhost)) {
navigator.serviceWorker.register('{{ .BaseURL }}/sw.js') navigator.serviceWorker.register('{{ .BaseURL }}/sw.js')
.then(function(registration) { .then(function(registration) {
// updatefound is fired if service-worker.js changes. // updatefound is fired if service-worker.js changes.
registration.onupdatefound = function() { registration.onupdatefound = function() {
// updatefound is also fired the very first time the SW is installed, // 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. // and there's no need to prompt for a reload at that point.
// So check here to see if the page is already controlled, // So check here to see if the page is already controlled,
// i.e. whether there's an existing service worker. // i.e. whether there's an existing service worker.
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
// The updatefound event implies that registration.installing is set // The updatefound event implies that registration.installing is set
const installingWorker = registration.installing; const installingWorker = registration.installing;
installingWorker.onstatechange = function() { installingWorker.onstatechange = function() {
switch (installingWorker.state) { switch (installingWorker.state) {
case 'installed': case 'installed':
// At this point, the old content will have been purged and the // At this point, the old content will have been purged and the
// fresh content will have been added to the cache. // fresh content will have been added to the cache.
// It's the perfect time to display a "New content is // It's the perfect time to display a "New content is
// available; please refresh." message in the page's interface. // available; please refresh." message in the page's interface.
break; break;
case 'redundant': case 'redundant':
throw new Error('The installing ' + throw new Error('The installing ' +
'service worker became redundant.'); 'service worker became redundant.');
default: default:
// Ignore // Ignore
} }
}; };
} }
}; };
}).catch(function(e) { }).catch(function(e) {
console.error('Error during service worker registration:', e); console.error('Error during service worker registration:', e);
}); });
} }
}); });
})(); })();

View File

@ -1,70 +1,70 @@
var path = require('path') var path = require('path')
var config = require('./config') var config = require('./config')
var ExtractTextPlugin = require('extract-text-webpack-plugin') var ExtractTextPlugin = require('extract-text-webpack-plugin')
exports.assetsPath = function (_path) { exports.assetsPath = function (_path) {
var assetsSubDirectory = config.assetsSubDirectory var assetsSubDirectory = config.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path) return path.posix.join(assetsSubDirectory, _path)
} }
exports.cssLoaders = function (options) { exports.cssLoaders = function (options) {
options = options || {} options = options || {}
var cssLoader = { var cssLoader = {
loader: 'css-loader', loader: 'css-loader',
options: { options: {
minimize: process.env.NODE_ENV === 'production', minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap sourceMap: options.sourceMap
} }
} }
// generate loader string to be used with extract text plugin // generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) { function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader] var loaders = [cssLoader]
if (loader) { if (loader) {
loaders.push({ loaders.push({
loader: loader + '-loader', loader: loader + '-loader',
options: Object.assign({}, loaderOptions, { options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap sourceMap: options.sourceMap
}) })
}) })
} }
// Extract CSS when that option is specified // Extract CSS when that option is specified
// (which is the case during production build) // (which is the case during production build)
if (options.extract) { if (options.extract) {
return ExtractTextPlugin.extract({ return ExtractTextPlugin.extract({
use: loaders, use: loaders,
fallback: 'vue-style-loader' fallback: 'vue-style-loader'
}) })
} else { } else {
return ['vue-style-loader'].concat(loaders) return ['vue-style-loader'].concat(loaders)
} }
} }
// https://vue-loader.vuejs.org/en/configurations/extract-css.html // https://vue-loader.vuejs.org/en/configurations/extract-css.html
return { return {
css: generateLoaders(), css: generateLoaders(),
postcss: generateLoaders(), postcss: generateLoaders(),
less: generateLoaders('less'), less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }), sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'), scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'), stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus') styl: generateLoaders('stylus')
} }
} }
// Generate loaders for standalone style files (outside of .vue) // Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) { exports.styleLoaders = function (options) {
var output = [] var output = []
var loaders = exports.cssLoaders(options) var loaders = exports.cssLoaders(options)
for (var extension in loaders) { for (var extension in loaders) {
var loader = loaders[extension] var loader = loaders[extension]
output.push({ output.push({
test: new RegExp('\\.' + extension + '$'), test: new RegExp('\\.' + extension + '$'),
use: loader use: loader
}) })
} }
return output return output
} }

View File

@ -1,12 +1,12 @@
var utils = require('./utils') var utils = require('./utils')
var config = require('./config') var config = require('./config')
var isProduction = process.env.NODE_ENV === 'production' var isProduction = process.env.NODE_ENV === 'production'
module.exports = { module.exports = {
loaders: utils.cssLoaders({ loaders: utils.cssLoaders({
sourceMap: isProduction sourceMap: isProduction
? config.build.productionSourceMap ? config.build.productionSourceMap
: config.dev.produceSourceMap, : config.dev.produceSourceMap,
extract: isProduction extract: isProduction
}) })
} }

View File

@ -1,69 +1,69 @@
var path = require('path') var path = require('path')
var utils = require('./utils') var utils = require('./utils')
var config = require('./config') var config = require('./config')
var vueLoaderConfig = require('./vue-loader.conf') var vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) { function resolve (dir) {
return path.join(__dirname, '..', dir) return path.join(__dirname, '..', dir)
} }
module.exports = { module.exports = {
entry: { entry: {
app: './assets/src/main.js' app: './assets/src/main.js'
}, },
output: { output: {
path: config.assetsRoot, path: config.assetsRoot,
filename: '[name].js', filename: '[name].js',
publicPath: config.assetsPublicPath publicPath: config.assetsPublicPath
}, },
resolve: { resolve: {
extensions: ['.js', '.vue', '.json'], extensions: ['.js', '.vue', '.json'],
alias: { alias: {
'vue$': 'vue/dist/vue.esm.js', 'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src') '@': resolve('src')
} }
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.(yml|yaml)$/, test: /\.(yml|yaml)$/,
loader: 'yml-loader' loader: 'yml-loader'
}, },
{ {
test: /\.(js|vue)$/, test: /\.(js|vue)$/,
loader: 'eslint-loader', loader: 'eslint-loader',
enforce: 'pre', enforce: 'pre',
include: [resolve('src'), resolve('test')], include: [resolve('src'), resolve('test')],
options: { options: {
formatter: require('eslint-friendly-formatter') formatter: require('eslint-friendly-formatter')
} }
}, },
{ {
test: /\.vue$/, test: /\.vue$/,
loader: 'vue-loader', loader: 'vue-loader',
options: vueLoaderConfig options: vueLoaderConfig
}, },
{ {
test: /\.js$/, test: /\.js$/,
loader: 'babel-loader', loader: 'babel-loader',
include: [resolve('src'), resolve('test')] include: [resolve('src'), resolve('test')]
}, },
{ {
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader', loader: 'url-loader',
options: { options: {
limit: 10000, limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]') name: utils.assetsPath('img/[name].[hash:7].[ext]')
} }
}, },
{ {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader', loader: 'url-loader',
options: { options: {
// limit: 10000, // limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]') name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
} }
} }
] ]
} }
} }

View File

@ -1,81 +1,81 @@
var fs = require('fs') var fs = require('fs')
var path = require('path') var path = require('path')
var utils = require('./utils') var utils = require('./utils')
var webpack = require('webpack') var webpack = require('webpack')
var config = require('./config') var config = require('./config')
var merge = require('webpack-merge') var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf') var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin') var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin') var ExtractTextPlugin = require('extract-text-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
var CopyWebpackPlugin = require('copy-webpack-plugin') var CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(baseWebpackConfig, { module.exports = merge(baseWebpackConfig, {
watch: true, watch: true,
module: { module: {
rules: utils.styleLoaders({ rules: utils.styleLoaders({
sourceMap: config.dev.produceSourceMap, sourceMap: config.dev.produceSourceMap,
extract: true extract: true
}) })
}, },
devtool: '#cheap-module-eval-source-map', devtool: '#cheap-module-eval-source-map',
output: { output: {
path: config.assetsRoot, path: config.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'), filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
}, },
plugins: [ plugins: [
new webpack.NoEmitOnErrorsPlugin(), new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin(), new FriendlyErrorsPlugin(),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': config.dev.env 'process.env': config.dev.env
}), }),
// extract css into its own file // extract css into its own file
new ExtractTextPlugin({ new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css') filename: utils.assetsPath('css/[name].[contenthash].css')
}), }),
// generate dist index.html with correct asset hash for caching. // generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html // you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin // see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: config.index, filename: config.index,
template: 'assets/index.html', template: 'assets/index.html',
inject: true, inject: true,
// necessary to consistently work with multiple chunks via CommonsChunkPlugin // necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency', chunksSortMode: 'dependency',
serviceWorkerLoader: `<script>${fs.readFileSync(path.join(__dirname, serviceWorkerLoader: `<script>${fs.readFileSync(path.join(__dirname,
'./service-worker-dev.js'), 'utf-8')}</script>` './service-worker-dev.js'), 'utf-8')}</script>`
}), }),
// split vendor js into its own file // split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({ new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', name: 'vendor',
minChunks: function (module, count) { minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor // any required modules inside node_modules are extracted to vendor
return ( return (
module.resource && module.resource &&
/\.js$/.test(module.resource) && /\.js$/.test(module.resource) &&
module.resource.indexOf( module.resource.indexOf(
path.join(__dirname, '../../node_modules') path.join(__dirname, '../../node_modules')
) === 0 ) === 0
) )
} }
}), }),
// extract webpack runtime and module manifest to its own file in order to // extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated // prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({ new webpack.optimize.CommonsChunkPlugin({
name: 'manifest', name: 'manifest',
chunks: ['vendor'] chunks: ['vendor']
}), }),
new CopyWebpackPlugin([ new CopyWebpackPlugin([
{ {
from: path.resolve(__dirname, '../static'), from: path.resolve(__dirname, '../static'),
to: config.assetsSubDirectory, to: config.assetsSubDirectory,
ignore: ['.*'] ignore: ['.*']
}, },
{ {
from: path.resolve(__dirname, '../../node_modules/codemirror/mode/*/*'), from: path.resolve(__dirname, '../../node_modules/codemirror/mode/*/*'),
to: path.join(config.assetsSubDirectory, 'js/codemirror/mode/[name]/[name].js') to: path.join(config.assetsSubDirectory, 'js/codemirror/mode/[name]/[name].js')
} }
]) ])
] ]
}) })

View File

@ -1,5 +1,5 @@
<svg id="content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 144"> <svg id="content" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 144">
<circle cx="72" cy="72" r="72" fill="#2979ff"/> <circle cx="72" cy="72" r="72" fill="#2979ff"/>
<circle cx="72" cy="72" r="48" fill="#40c4ff"/> <circle cx="72" cy="72" r="48" fill="#40c4ff"/>
<circle cx="72" cy="72" r="24" fill="#fff"/> <circle cx="72" cy="72" r="24" fill="#fff"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 239 B

View File

@ -1,22 +1,22 @@
<template> <template>
<select v-on:change="change" :value="selected"> <select v-on:change="change" :value="selected">
<option value="en">{{ $t('languages.en') }}</option> <option value="en">{{ $t('languages.en') }}</option>
<option value="fr">{{ $t('languages.fr') }}</option> <option value="fr">{{ $t('languages.fr') }}</option>
<option value="pt">{{ $t('languages.pt') }}</option> <option value="pt">{{ $t('languages.pt') }}</option>
<option value="ja">{{ $t('languages.ja') }}</option> <option value="ja">{{ $t('languages.ja') }}</option>
<option value="zh-cn">{{ $t('languages.zhCN') }}</option> <option value="zh-cn">{{ $t('languages.zhCN') }}</option>
<option value="zh-tw">{{ $t('languages.zhTW') }}</option> <option value="zh-tw">{{ $t('languages.zhTW') }}</option>
</select> </select>
</template> </template>
<script> <script>
export default { export default {
name: 'languages', name: 'languages',
props: [ 'selected' ], props: [ 'selected' ],
methods: { methods: {
change (event) { change (event) {
this.$emit('update:selected', event.target.value) this.$emit('update:selected', event.target.value)
} }
} }
} }
</script> </script>

View File

@ -1,265 +1,265 @@
<template> <template>
<div id="search" @click="open" v-bind:class="{ active , ongoing }"> <div id="search" @click="open" v-bind:class="{ active , ongoing }">
<div id="input"> <div id="input">
<button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')"> <button v-if="active" class="action" @click="close" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
<i class="material-icons">arrow_back</i> <i class="material-icons">arrow_back</i>
</button> </button>
<i v-else class="material-icons">search</i> <i v-else class="material-icons">search</i>
<input type="text" <input type="text"
@keyup="keyup" @keyup="keyup"
@keyup.enter="submit" @keyup.enter="submit"
ref="input" ref="input"
:autofocus="active" :autofocus="active"
v-model.trim="value" v-model.trim="value"
:aria-label="$t('search.writeToSearch')" :aria-label="$t('search.writeToSearch')"
:placeholder="placeholder"> :placeholder="placeholder">
</div> </div>
<div id="result"> <div id="result">
<div> <div>
<template v-if="search.length === 0 && commands.length === 0"> <template v-if="search.length === 0 && commands.length === 0">
<p>{{ text }}</p> <p>{{ text }}</p>
<template v-if="value.length === 0"> <template v-if="value.length === 0">
<div class="boxes"> <div class="boxes">
<h3>{{ $t('search.types') }}</h3> <h3>{{ $t('search.types') }}</h3>
<div> <div>
<div tabindex="0" <div tabindex="0"
role="button" role="button"
@click="init('type:image')" @click="init('type:image')"
:aria-label="$t('search.images')"> :aria-label="$t('search.images')">
<i class="material-icons">insert_photo</i> <i class="material-icons">insert_photo</i>
<p>{{ $t('search.images') }}</p> <p>{{ $t('search.images') }}</p>
</div> </div>
<div tabindex="0" <div tabindex="0"
role="button" role="button"
@click="init('type:audio')" @click="init('type:audio')"
:aria-label="$t('search.music')"> :aria-label="$t('search.music')">
<i class="material-icons">volume_up</i> <i class="material-icons">volume_up</i>
<p>{{ $t('search.music') }}</p> <p>{{ $t('search.music') }}</p>
</div> </div>
<div tabindex="0" <div tabindex="0"
role="button" role="button"
@click="init('type:video')" @click="init('type:video')"
:aria-label="$t('search.video')"> :aria-label="$t('search.video')">
<i class="material-icons">movie</i> <i class="material-icons">movie</i>
<p>{{ $t('search.video') }}</p> <p>{{ $t('search.video') }}</p>
</div> </div>
<div tabindex="0" <div tabindex="0"
role="button" role="button"
@click="init('type:pdf')" @click="init('type:pdf')"
:aria-label="$t('search.pdf')"> :aria-label="$t('search.pdf')">
<i class="material-icons">picture_as_pdf</i> <i class="material-icons">picture_as_pdf</i>
<p>{{ $t('search.pdf') }}</p> <p>{{ $t('search.pdf') }}</p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</template> </template>
<ul v-else-if="search.length > 0"> <ul v-else-if="search.length > 0">
<li v-for="s in search"> <li v-for="s in search">
<router-link @click.native="close" :to="'./' + s.path"> <router-link @click.native="close" :to="'./' + s.path">
<i v-if="s.dir" class="material-icons">folder</i> <i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i> <i v-else class="material-icons">insert_drive_file</i>
<span>./{{ s.path }}</span> <span>./{{ s.path }}</span>
</router-link> </router-link>
</li> </li>
</ul> </ul>
<pre v-else-if="commands.length > 0"> <pre v-else-if="commands.length > 0">
<template v-for="c in commands">{{ c }}</template> <template v-for="c in commands">{{ c }}</template>
</pre> </pre>
</div> </div>
<p id="renew"><i class="material-icons spin">autorenew</i></p> <p id="renew"><i class="material-icons spin">autorenew</i></p>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import url from '@/utils/url' import url from '@/utils/url'
import * as api from '@/utils/api' import * as api from '@/utils/api'
export default { export default {
name: 'search', name: 'search',
data: function () { data: function () {
return { return {
value: '', value: '',
active: false, active: false,
ongoing: false, ongoing: false,
scrollable: null, scrollable: null,
search: [], search: [],
commands: [], commands: [],
reload: false reload: false
} }
}, },
watch: { watch: {
show (val, old) { show (val, old) {
this.active = (val === 'search') this.active = (val === 'search')
// If the hover was search and now it's something else // If the hover was search and now it's something else
// we should blur the input. // we should blur the input.
if (old === 'search' && val !== 'search') { if (old === 'search' && val !== 'search') {
if (this.reload) { if (this.reload) {
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
} }
document.body.style.overflow = 'auto' document.body.style.overflow = 'auto'
this.reset() this.reset()
this.$refs.input.blur() this.$refs.input.blur()
} }
// If we are starting to show the search box, we should // If we are starting to show the search box, we should
// focus the input. // focus the input.
if (val === 'search') { if (val === 'search') {
this.reload = false this.reload = false
this.$refs.input.focus() this.$refs.input.focus()
document.body.style.overflow = 'hidden' document.body.style.overflow = 'hidden'
} }
} }
}, },
computed: { computed: {
...mapState(['user', 'show']), ...mapState(['user', 'show']),
// Placeholder value. // Placeholder value.
placeholder: function () { placeholder: function () {
if (this.user.allowCommands && this.user.commands.length > 0) { if (this.user.allowCommands && this.user.commands.length > 0) {
return this.$t('search.searchOrCommand') return this.$t('search.searchOrCommand')
} }
return this.$t('search.search') return this.$t('search.search')
}, },
// The text that is shown on the results' box while // The text that is shown on the results' box while
// there is no search result or command output to show. // there is no search result or command output to show.
text: function () { text: function () {
if (this.ongoing) { if (this.ongoing) {
return '' return ''
} }
if (this.value.length === 0) { if (this.value.length === 0) {
if (this.user.allowCommands && this.user.commands.length > 0) { if (this.user.allowCommands && this.user.commands.length > 0) {
return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.` return `${this.$t('search.searchOrSupportedCommand')} ${this.user.commands.join(', ')}.`
} }
this.$t('search.type') this.$t('search.type')
} }
if (!this.supported() || !this.user.allowCommands) { if (!this.supported() || !this.user.allowCommands) {
return this.$t('search.pressToSearch') return this.$t('search.pressToSearch')
} else { } else {
return this.$t('search.pressToExecute') return this.$t('search.pressToExecute')
} }
} }
}, },
mounted: function () { mounted: function () {
// Gets the result div which will be scrollable. // Gets the result div which will be scrollable.
this.scrollable = document.querySelector('#search #result') this.scrollable = document.querySelector('#search #result')
// Adds the keydown event on window for the ESC key, so // Adds the keydown event on window for the ESC key, so
// when it's pressed, it closes the search window. // when it's pressed, it closes the search window.
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if (event.keyCode === 27) { if (event.keyCode === 27) {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
} }
}) })
}, },
methods: { methods: {
// Sets the search to active. // Sets the search to active.
open (event) { open (event) {
this.$store.commit('showHover', 'search') this.$store.commit('showHover', 'search')
}, },
// Closes the search and prevents the event // Closes the search and prevents the event
// of propagating so it doesn't trigger the // of propagating so it doesn't trigger the
// click event on #search. // click event on #search.
close (event) { close (event) {
event.stopPropagation() event.stopPropagation()
event.preventDefault() event.preventDefault()
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
}, },
// Checks if the current input is a supported command. // Checks if the current input is a supported command.
supported () { supported () {
let pieces = this.value.split(' ') let pieces = this.value.split(' ')
for (let i = 0; i < this.user.commands.length; i++) { for (let i = 0; i < this.user.commands.length; i++) {
if (pieces[0] === this.user.commands[i]) { if (pieces[0] === this.user.commands[i]) {
return true return true
} }
} }
return false return false
}, },
// Initializes the search with a default value. // Initializes the search with a default value.
init (string) { init (string) {
this.value = string + ' ' this.value = string + ' '
this.$refs.input.focus() this.$refs.input.focus()
}, },
// Resets the search box value. // Resets the search box value.
reset () { reset () {
this.value = '' this.value = ''
this.active = false this.active = false
this.ongoing = false this.ongoing = false
this.search = [] this.search = []
this.commands = [] this.commands = []
}, },
// When the user presses a key, if it is ESC // When the user presses a key, if it is ESC
// then it will close the search box. Otherwise, // then it will close the search box. Otherwise,
// it will set the search box to active and clean // it will set the search box to active and clean
// the search results, as well as commands'. // the search results, as well as commands'.
keyup (event) { keyup (event) {
if (event.keyCode === 27) { if (event.keyCode === 27) {
this.close(event) this.close(event)
return return
} }
this.search.length = 0 this.search.length = 0
this.commands.length = 0 this.commands.length = 0
}, },
// Submits the input to the server and sets ongoing to true. // Submits the input to the server and sets ongoing to true.
submit (event) { submit (event) {
this.ongoing = true this.ongoing = true
let path = this.$route.path let path = this.$route.path
if (this.$store.state.req.kind !== 'listing') { if (this.$store.state.req.kind !== 'listing') {
path = url.removeLastDir(path) + '/' path = url.removeLastDir(path) + '/'
} }
// In case of being a command. // In case of being a command.
if (this.supported() && this.user.allowCommands) { if (this.supported() && this.user.allowCommands) {
api.command(path, this.value, api.command(path, this.value,
(event) => { (event) => {
this.commands.push(event.data) this.commands.push(event.data)
this.scrollable.scrollTop = this.scrollable.scrollHeight this.scrollable.scrollTop = this.scrollable.scrollHeight
}, },
(event) => { (event) => {
this.reload = true this.reload = true
this.ongoing = false this.ongoing = false
this.scrollable.scrollTop = this.scrollable.scrollHeight this.scrollable.scrollTop = this.scrollable.scrollHeight
} }
) )
return return
} }
// In case of being a search. // In case of being a search.
api.search(path, this.value, api.search(path, this.value,
(event) => { (event) => {
let response = JSON.parse(event.data) let response = JSON.parse(event.data)
if (response.path[0] === '/') { if (response.path[0] === '/') {
response.path = response.path.substring(1) response.path = response.path.substring(1)
} }
this.search.push(response) this.search.push(response)
this.scrollable.scrollTop = this.scrollable.scrollHeight this.scrollable.scrollTop = this.scrollable.scrollHeight
}, },
(event) => { (event) => {
this.ongoing = false this.ongoing = false
this.scrollable.scrollTop = this.scrollable.scrollHeight this.scrollable.scrollTop = this.scrollable.scrollHeight
} }
) )
} }
} }
} }
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button"> <button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
<i class="material-icons">content_copy</i> <i class="material-icons">content_copy</i>
<span>{{ $t('buttons.copyFile') }}</span> <span>{{ $t('buttons.copyFile') }}</span>
</button> </button>
</template> </template>
<script> <script>
export default { export default {
name: 'copy-button', name: 'copy-button',
methods: { methods: {
show: function (event) { show: function (event) {
this.$store.commit('showHover', 'copy') this.$store.commit('showHover', 'copy')
} }
} }
} }
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button"> <button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
<i class="material-icons">delete</i> <i class="material-icons">delete</i>
<span>{{ $t('buttons.delete') }}</span> <span>{{ $t('buttons.delete') }}</span>
</button> </button>
</template> </template>
<script> <script>
export default { export default {
name: 'delete-button', name: 'delete-button',
methods: { methods: {
show: function (event) { show: function (event) {
this.$store.commit('showHover', 'delete') this.$store.commit('showHover', 'delete')
} }
} }
} }
</script> </script>

View File

@ -1,39 +1,39 @@
<template> <template>
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action"> <button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
<i class="material-icons">file_download</i> <i class="material-icons">file_download</i>
<span>{{ $t('buttons.download') }}</span> <span>{{ $t('buttons.download') }}</span>
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span> <span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
</button> </button>
</template> </template>
<script> <script>
import {mapGetters, mapState} from 'vuex' import {mapGetters, mapState} from 'vuex'
import * as api from '@/utils/api' import * as api from '@/utils/api'
export default { export default {
name: 'download-button', name: 'download-button',
computed: { computed: {
...mapState(['req', 'selected']), ...mapState(['req', 'selected']),
...mapGetters(['selectedCount']) ...mapGetters(['selectedCount'])
}, },
methods: { methods: {
download: function (event) { download: function (event) {
// If we are not on a listing, download the current file. // If we are not on a listing, download the current file.
if (this.req.kind !== 'listing') { if (this.req.kind !== 'listing') {
api.download(null, this.$route.path) api.download(null, this.$route.path)
return return
} }
// If we are on a listing and there is one element selected, // If we are on a listing and there is one element selected,
// download it. // download it.
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) { if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.req.items[this.selected[0]].url) api.download(null, this.req.items[this.selected[0]].url)
return return
} }
// Otherwise show the prompt to choose the formt of the download. // Otherwise show the prompt to choose the formt of the download.
this.$store.commit('showHover', 'download') this.$store.commit('showHover', 'download')
} }
} }
} }
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show"> <button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
<i class="material-icons">info</i> <i class="material-icons">info</i>
<span>{{ $t('buttons.info') }}</span> <span>{{ $t('buttons.info') }}</span>
</button> </button>
</template> </template>
<script> <script>
export default { export default {
name: 'info-button', name: 'info-button',
methods: { methods: {
show: function (event) { show: function (event) {
this.$store.commit('showHover', 'info') this.$store.commit('showHover', 'info')
} }
} }
} }
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button"> <button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
<i class="material-icons">forward</i> <i class="material-icons">forward</i>
<span>{{ $t('buttons.moveFile') }}</span> <span>{{ $t('buttons.moveFile') }}</span>
</button> </button>
</template> </template>
<script> <script>
export default { export default {
name: 'move-button', name: 'move-button',
methods: { methods: {
show: function (event) { show: function (event) {
this.$store.commit('showHover', 'move') this.$store.commit('showHover', 'move')
} }
} }
} }
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button"> <button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
<i class="material-icons">mode_edit</i> <i class="material-icons">mode_edit</i>
<span>{{ $t('buttons.rename') }}</span> <span>{{ $t('buttons.rename') }}</span>
</button> </button>
</template> </template>
<script> <script>
export default { export default {
name: 'rename-button', name: 'rename-button',
methods: { methods: {
show: function (event) { show: function (event) {
this.$store.commit('showHover', 'rename') this.$store.commit('showHover', 'rename')
} }
} }
} }
</script> </script>

View File

@ -1,21 +1,21 @@
<template> <template>
<button @click="show" <button @click="show"
:aria-label="$t('buttons.schedule')" :aria-label="$t('buttons.schedule')"
:title="$t('buttons.schedule')" :title="$t('buttons.schedule')"
id="schedule-button" id="schedule-button"
class="action"> class="action">
<i class="material-icons">alarm</i> <i class="material-icons">alarm</i>
<span>{{ $t('buttons.schedule') }}</span> <span>{{ $t('buttons.schedule') }}</span>
</button> </button>
</template> </template>
<script> <script>
export default { export default {
name: 'schedule-button', name: 'schedule-button',
methods: { methods: {
show: function (event) { show: function (event) {
this.$store.commit('showHover', 'schedule') this.$store.commit('showHover', 'schedule')
} }
} }
} }
</script> </script>

View File

@ -1,17 +1,17 @@
<template> <template>
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button"> <button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
<i class="material-icons">file_upload</i> <i class="material-icons">file_upload</i>
<span>{{ $t('buttons.upload') }}</span> <span>{{ $t('buttons.upload') }}</span>
</button> </button>
</template> </template>
<script> <script>
export default { export default {
name: 'upload-button', name: 'upload-button',
methods: { methods: {
upload: function (event) { upload: function (event) {
document.getElementById('upload-input').click() document.getElementById('upload-input').click()
} }
} }
} }
</script> </script>

View File

@ -119,9 +119,21 @@ export default {
} }
if (event.shiftKey && this.selected.length === 1) { if (event.shiftKey && this.selected.length === 1) {
let fi = (this.index > this.selected[0]) ? this.selected[0] : this.index let fi = 0
let la = (this.index > this.selected[0]) ? this.index : this.selected[0] let la = 0
for (; fi <= la; fi++) this.addSelected(fi)
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 return
} }

View File

@ -1,84 +1,84 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t('prompts.rename') }}</h2> <h2>{{ $t('prompts.rename') }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p> <p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<input autofocus type="text" @keyup.enter="submit" v-model.trim="name"> <input autofocus type="text" @keyup.enter="submit" v-model.trim="name">
</div> </div>
<div class="card-action"> <div class="card-action">
<button class="cancel flat" <button class="cancel flat"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button> :title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit" <button @click="submit"
class="flat" class="flat"
type="submit" type="submit"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button> :title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import url from '@/utils/url' import url from '@/utils/url'
import * as api from '@/utils/api' import * as api from '@/utils/api'
export default { export default {
name: 'rename', name: 'rename',
data: function () { data: function () {
return { return {
name: '' name: ''
} }
}, },
computed: mapState(['req', 'selected', 'selectedCount']), computed: mapState(['req', 'selected', 'selectedCount']),
methods: { methods: {
cancel: function (event) { cancel: function (event) {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
}, },
oldName: function () { oldName: function () {
// Get the current name of the file we are editing. // Get the current name of the file we are editing.
if (this.req.kind !== 'listing') { if (this.req.kind !== 'listing') {
return this.req.name return this.req.name
} }
if (this.selectedCount === 0 || this.selectedCount > 1) { if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen. // This shouldn't happen.
return return
} }
return this.req.items[this.selected[0]].name return this.req.items[this.selected[0]].name
}, },
submit: function (event) { submit: function (event) {
let oldLink = '' let oldLink = ''
let newLink = '' let newLink = ''
if (this.req.kind !== 'listing') { if (this.req.kind !== 'listing') {
oldLink = this.req.url oldLink = this.req.url
} else { } else {
oldLink = this.req.items[this.selected[0]].url oldLink = this.req.items[this.selected[0]].url
} }
this.name = encodeURIComponent(this.name) this.name = encodeURIComponent(this.name)
newLink = url.removeLastDir(oldLink) + '/' + this.name newLink = url.removeLastDir(oldLink) + '/' + this.name
api.move([{ from: oldLink, to: newLink }]) api.move([{ from: oldLink, to: newLink }])
.then(() => { .then(() => {
if (this.req.kind !== 'listing') { if (this.req.kind !== 'listing') {
this.$router.push({ path: newLink }) this.$router.push({ path: newLink })
return return
} }
this.$store.commit('setReload', true) this.$store.commit('setReload', true)
}).catch(error => { }).catch(error => {
this.$showError(error) this.$showError(error)
}) })
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
} }
} }
} }
</script> </script>

View File

@ -1,184 +1,184 @@
@import "~codemirror/lib/codemirror.css"; @import "~codemirror/lib/codemirror.css";
@import "~codemirror/theme/ttcn.css"; @import "~codemirror/theme/ttcn.css";
#editor { #editor {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
} }
#editor .CodeMirror { #editor .CodeMirror {
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
margin: 2em 0; margin: 2em 0;
border-radius: .5em; border-radius: .5em;
} }
#editor h2 { #editor h2 {
color: rgba(0, 0, 0, 0.3); color: rgba(0, 0, 0, 0.3);
font-weight: 500; font-weight: 500;
} }
.CodeMirror { .CodeMirror {
height: auto; height: auto;
} }
.markdown .CodeMirror { .markdown .CodeMirror {
padding: .75em; padding: .75em;
} }
.cm-s-markdown .CodeMirror-gutter { .cm-s-markdown .CodeMirror-gutter {
border-right: 1px solid #eff3f5; border-right: 1px solid #eff3f5;
padding-right: 5px; padding-right: 5px;
margin-right: 15px; margin-right: 15px;
min-width: 2.5em; min-width: 2.5em;
padding-bottom: 30px; padding-bottom: 30px;
} }
.cm-s-markdown .CodeMirror-cursor { .cm-s-markdown .CodeMirror-cursor {
border-right: 2px solid #667880; border-right: 2px solid #667880;
} }
.cm-s-markdown .CodeMirror-lines { .cm-s-markdown .CodeMirror-lines {
margin: 0; margin: 0;
} }
.cm-s-markdown { .cm-s-markdown {
color: #3D494E; color: #3D494E;
} }
.cm-s-markdown span.cm-header { .cm-s-markdown span.cm-header {
color: #3D494E; color: #3D494E;
font-weight: bold; font-weight: bold;
} }
.cm-s-markdown span.cm-variable-2 { .cm-s-markdown span.cm-variable-2 {
color: #3D494E; color: #3D494E;
} }
.cm-s-markdown span.cm-meta { .cm-s-markdown span.cm-meta {
color: #516066; color: #516066;
} }
.cm-s-markdown span.cm-hr { .cm-s-markdown span.cm-hr {
color: #516066; color: #516066;
} }
.cm-s-markdown span.cm-comment { .cm-s-markdown span.cm-comment {
color: #868f93; color: #868f93;
} }
.cm-s-markdown span.cm-qualifier { .cm-s-markdown span.cm-qualifier {
color: #868f93; color: #868f93;
} }
.cm-s-markdown span.cm-number { .cm-s-markdown span.cm-number {
color: #197987; color: #197987;
} }
.cm-s-markdown span.cm-variable { .cm-s-markdown span.cm-variable {
color: #197987; color: #197987;
} }
.cm-s-markdown span.cm-builtin { .cm-s-markdown span.cm-builtin {
color: #197987; color: #197987;
} }
.cm-s-markdown span.cm-link { .cm-s-markdown span.cm-link {
color: #197987; color: #197987;
text-decoration: underline; text-decoration: underline;
} }
.cm-s-markdown span.cm-tag { .cm-s-markdown span.cm-tag {
color: #197987; color: #197987;
} }
.cm-s-markdown span.cm-string { .cm-s-markdown span.cm-string {
color: #48abb9; color: #48abb9;
} }
.cm-s-markdown span.cm-string-2 { .cm-s-markdown span.cm-string-2 {
color: #48abb9; color: #48abb9;
} }
.cm-s-markdown span.cm-quote { .cm-s-markdown span.cm-quote {
color: #48abb9; color: #48abb9;
} }
.cm-s-markdown span.cm-atom { .cm-s-markdown span.cm-atom {
color: #48abb9; color: #48abb9;
} }
.cm-s-markdown span.cm-property { .cm-s-markdown span.cm-property {
color: #82a367; color: #82a367;
} }
.cm-s-markdown span.cm-operator { .cm-s-markdown span.cm-operator {
color: #82a367; color: #82a367;
} }
.cm-s-markdown span.cm-variable-3 { .cm-s-markdown span.cm-variable-3 {
color: #82a367; color: #82a367;
} }
.cm-s-markdown span.cm-attribute { .cm-s-markdown span.cm-attribute {
color: #90bb74; color: #90bb74;
} }
.cm-s-markdown span.cm-def { .cm-s-markdown span.cm-def {
color: #90bb74; color: #90bb74;
} }
.cm-s-markdown span.cm-keyword { .cm-s-markdown span.cm-keyword {
color: #ec6c45; color: #ec6c45;
} }
.cm-s-markdown span.cm-bracket { .cm-s-markdown span.cm-bracket {
color: #ec6c45; color: #ec6c45;
} }
.cm-s-markdown span.cm-error { .cm-s-markdown span.cm-error {
color: #e45346; color: #e45346;
} }
.cm-s-markdown span.cm-em { .cm-s-markdown span.cm-em {
font-style: italic; font-style: italic;
} }
.cm-s-markdown span.cm-strong { .cm-s-markdown span.cm-strong {
font-weight: bold; font-weight: bold;
} }
.cm-s-markdown .cm-header-1 { .cm-s-markdown .cm-header-1 {
font-size: 200%; font-size: 200%;
line-height: 200%; line-height: 200%;
} }
.cm-s-markdown .cm-header-2 { .cm-s-markdown .cm-header-2 {
font-size: 160%; font-size: 160%;
line-height: 160%; line-height: 160%;
} }
.cm-s-markdown .cm-header-3 { .cm-s-markdown .cm-header-3 {
font-size: 125%; font-size: 125%;
line-height: 125%; line-height: 125%;
} }
.cm-s-markdown .cm-header-4 { .cm-s-markdown .cm-header-4 {
font-size: 110%; font-size: 110%;
line-height: 110%; line-height: 110%;
} }
.cm-s-markdown .cm-comment { .cm-s-markdown .cm-comment {
background: rgba(0, 0, 0, .05); background: rgba(0, 0, 0, .05);
border-radius: 2px; border-radius: 2px;
} }
.cm-s-markdown .cm-link { .cm-s-markdown .cm-link {
color: #7f8c8d; color: #7f8c8d;
} }
.cm-s-markdown .cm-url { .cm-s-markdown .cm-url {
color: #aab2b3; color: #aab2b3;
} }
.cm-s-markdown .cm-strikethrough { .cm-s-markdown .cm-strikethrough {
text-decoration: line-through; text-decoration: line-through;
} }

View File

@ -1,137 +1,137 @@
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format('woff2'); 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; unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic.woff2) format('woff2'); 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek-ext.woff2) format('woff2'); src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek-ext.woff2) format('woff2');
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek.woff2) format('woff2'); src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek.woff2) format('woff2');
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-vietnamese.woff2) format('woff2'); 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; unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin-ext.woff2) format('woff2'); 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; unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin.woff2) format('woff2'); 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; 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-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format('woff2'); 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; unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic.woff2) format('woff2'); 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; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek-ext.woff2) format('woff2'); src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek-ext.woff2) format('woff2');
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek.woff2) format('woff2'); src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek.woff2) format('woff2');
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-vietnamese.woff2) format('woff2'); 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; unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin-ext.woff2) format('woff2'); 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; unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin.woff2) format('woff2'); 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; 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-face {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Material Icons'), local('MaterialIcons-Regular'), url(../assets/fonts/material/icons.woff2) format('woff2'); src: local('Material Icons'), local('MaterialIcons-Regular'), url(../assets/fonts/material/icons.woff2) format('woff2');
} }
.prompt .file-list ul li:before, .prompt .file-list ul li:before,
.material-icons { .material-icons {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-size: 24px; font-size: 24px;
line-height: 1; line-height: 1;
letter-spacing: normal; letter-spacing: normal;
text-transform: none; text-transform: none;
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
word-wrap: normal; word-wrap: normal;
direction: ltr; direction: ltr;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga'; font-feature-settings: 'liga';
} }

View File

@ -1,113 +1,113 @@
@media (max-width: 1024px) { @media (max-width: 1024px) {
nav { nav {
width: 10em width: 10em
} }
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
#listing.list .item.header, #listing.list .item.header,
main { main {
width: calc(100% - 13em) width: calc(100% - 13em)
} }
} }
@media (max-width: 736px) { @media (max-width: 736px) {
#more { #more {
display: inherit display: inherit
} }
header .overlay { header .overlay {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
} }
#dropdown { #dropdown {
position: fixed; position: fixed;
top: 1em; top: 1em;
right: 1em; right: 1em;
display: block; display: block;
background-color: #fff; background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transform: scale(0); transform: scale(0);
transition: .1s ease-in-out transform; transition: .1s ease-in-out transform;
transform-origin: top right; transform-origin: top right;
z-index: 99999; z-index: 99999;
} }
#dropdown > div { #dropdown > div {
display: block; display: block;
} }
#dropdown.active { #dropdown.active {
transform: scale(1); transform: scale(1);
} }
#dropdown .action { #dropdown .action {
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 0; border-radius: 0;
width: 100%; width: 100%;
} }
#dropdown .action span:not(.counter) { #dropdown .action span:not(.counter) {
display: inline-block; display: inline-block;
padding: .4em; padding: .4em;
} }
#dropdown .counter { #dropdown .counter {
left: 2.25em; left: 2.25em;
} }
#file-selection { #file-selection {
position: fixed; position: fixed;
bottom: 1em; bottom: 1em;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;
align-items: center; align-items: center;
background: #fff; background: #fff;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
width: 95%; width: 95%;
max-width: 20em; max-width: 20em;
} }
#file-selection .action { #file-selection .action {
border-radius: 50%; border-radius: 50%;
width: auto; width: auto;
} }
#file-selection > span { #file-selection > span {
display: inline-block; display: inline-block;
margin-left: 1em; margin-left: 1em;
color: #6f6f6f; color: #6f6f6f;
margin-right: auto; margin-right: auto;
} }
nav { nav {
top: 0; top: 0;
z-index: 99999; z-index: 99999;
background: #fff; background: #fff;
height: 100%; height: 100%;
width: 16em; width: 16em;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .1s ease left; transition: .1s ease left;
left: -17em; left: -17em;
} }
nav.active { nav.active {
left: 0; left: 0;
} }
header .search-button, header .search-button,
header>div:first-child>.action { header>div:first-child>.action {
display: inherit; display: inherit;
} }
header img { header img {
display: none; display: none;
} }
#listing { #listing {
margin-bottom: 5em; margin-bottom: 5em;
} }
#listing.list .item.header, #listing.list .item.header,
main { main {
width: calc(100% - 2em); width: calc(100% - 2em);
} }
main { main {
margin: 0 1em; margin: 0 1em;
width: calc(100% - 2em); width: calc(100% - 2em);
} }
#search { #search {
display: none; display: none;
} }
#search.active { #search.active {
display: block; display: block;
} }
} }

View File

@ -1,200 +1,200 @@
permanent: 永久 permanent: 永久
buttons: buttons:
cancel: キャンセル cancel: キャンセル
close: 閉じる close: 閉じる
copy: コピー copy: コピー
copyFile: ファイルをコピー copyFile: ファイルをコピー
copyToClipboard: クリップボードにコピー copyToClipboard: クリップボードにコピー
create: 作成 create: 作成
delete: 削除 delete: 削除
download: ダウンロード download: ダウンロード
info: 情報 info: 情報
more: More more: More
move: 移動 move: 移動
moveFile: ファイルを移動 moveFile: ファイルを移動
new: 新規 new: 新規
next: next:
ok: OK ok: OK
replace: 置き換える replace: 置き換える
previous: previous:
rename: 名前を変更 rename: 名前を変更
reportIssue: 問題を報告 reportIssue: 問題を報告
save: 保存 save: 保存
search: 検索 search: 検索
select: 選択 select: 選択
share: シェア share: シェア
publish: 発表 publish: 発表
selectMultiple: 複数選択 selectMultiple: 複数選択
schedule: スケジュール schedule: スケジュール
switchView: 表示を切り替わる switchView: 表示を切り替わる
toggleSidebar: サイドバーを表示する toggleSidebar: サイドバーを表示する
update: 更新 update: 更新
upload: アップロード upload: アップロード
permalink: 固定リンク permalink: 固定リンク
success: success:
linkCopied: リンクがコピーされました! linkCopied: リンクがコピーされました!
errors: errors:
forbidden: アクセスが拒否されました。 forbidden: アクセスが拒否されました。
internal: 内部エラーが発生しました。 internal: 内部エラーが発生しました。
notFound: リソースが見つからなりませんでした。 notFound: リソースが見つからなりませんでした。
files: files:
folders: フォルダ folders: フォルダ
files: ファイル files: ファイル
body: 本文 body: 本文
clear: クリアー clear: クリアー
closePreview: プレビューを閉じる closePreview: プレビューを閉じる
home: ホーム home: ホーム
lastModified: 最終変更 lastModified: 最終変更
loading: ローディング... loading: ローディング...
lonely: ここには何もない... lonely: ここには何もない...
metadata: メタデータ metadata: メタデータ
multipleSelectionEnabled: 複数選択有効 multipleSelectionEnabled: 複数選択有効
name: 名前 name: 名前
size: サイズ size: サイズ
sortByName: 名前によるソート sortByName: 名前によるソート
sortBySize: サイズによるソート sortBySize: サイズによるソート
sortByLastModified: 最終変更日付によるソート sortByLastModified: 最終変更日付によるソート
help: help:
click: ファイルやディレクトリを選択 click: ファイルやディレクトリを選択
ctrl: ctrl:
click: 複数のファイルやディレクトリを選択 click: 複数のファイルやディレクトリを選択
f: 検索を有効にする f: 検索を有効にする
s: ファイルを保存またはカレントディレクトリをダウンロード s: ファイルを保存またはカレントディレクトリをダウンロード
del: 選択した項目を削除 del: 選択した項目を削除
doubleClick: ファイルやディレクトリをオープン doubleClick: ファイルやディレクトリをオープン
esc: 選択をクリアーまたはプロンプトを閉じる esc: 選択をクリアーまたはプロンプトを閉じる
f1: このヘルプを表示 f1: このヘルプを表示
f2: ファイルの名前を変更 f2: ファイルの名前を変更
help: ヘルプ help: ヘルプ
login: login:
password: パスワード password: パスワード
submit: ログイン submit: ログイン
username: ユーザ名 username: ユーザ名
wrongCredentials: ユーザ名またはパスワードが間違っています。 wrongCredentials: ユーザ名またはパスワードが間違っています。
prompts: prompts:
copy: コピー copy: コピー
copyMessage: コピーの目標ディレクトリを選択してください: copyMessage: コピーの目標ディレクトリを選択してください:
currentlyNavigating: 現在閲覧しているディレクトリ: currentlyNavigating: 現在閲覧しているディレクトリ:
deleteMessageMultiple: '{count} つのファイルを本当に削除してよろしいですか。' deleteMessageMultiple: '{count} つのファイルを本当に削除してよろしいですか。'
deleteMessageSingle: このファイル/フォルダを本当に削除してよろしいですか。 deleteMessageSingle: このファイル/フォルダを本当に削除してよろしいですか。
deleteTitle: ファイルを削除 deleteTitle: ファイルを削除
displayName: 名前: displayName: 名前:
download: ファイルをダウンロード download: ファイルをダウンロード
downloadMessage: 圧縮形式を選択してください。 downloadMessage: 圧縮形式を選択してください。
error: あるエラーが発生しました。 error: あるエラーが発生しました。
fileInfo: ファイル情報 fileInfo: ファイル情報
filesSelected: '{count} つのファイルは選択されました。' filesSelected: '{count} つのファイルは選択されました。'
lastModified: 最終変更 lastModified: 最終変更
move: 移動 move: 移動
moveMessage: 移動の目標ディレクトリを選択してください: moveMessage: 移動の目標ディレクトリを選択してください:
newDir: 新しいディレクトリを作成 newDir: 新しいディレクトリを作成
newDirMessage: 新しいディレクトリの名前を入力してください。 newDirMessage: 新しいディレクトリの名前を入力してください。
newFile: 新しいファイルを作成 newFile: 新しいファイルを作成
newFileMessage: 新しいファイルの名前を入力してください。 newFileMessage: 新しいファイルの名前を入力してください。
numberDirs: ディレクトリ個数 numberDirs: ディレクトリ個数
numberFiles: ファイル個数 numberFiles: ファイル個数
replace: 置き換える replace: 置き換える
replaceMessage: > replaceMessage: >
アップロードするファイルの中でかち合う名前が一つあります。 アップロードするファイルの中でかち合う名前が一つあります。
既存のファイルを置き換えりませんか。 既存のファイルを置き換えりませんか。
rename: 名前を変更 rename: 名前を変更
renameMessage: 名前を変更しようファイルは: renameMessage: 名前を変更しようファイルは:
show: 表示 show: 表示
size: サイズ size: サイズ
schedule: スケジュール schedule: スケジュール
scheduleMessage: このポストの発表日付をスケジュールしてください。 scheduleMessage: このポストの発表日付をスケジュールしてください。
newArchetype: ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。 newArchetype: ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。
settings: settings:
admin: 管理者 admin: 管理者
administrator: 管理者 administrator: 管理者
allowCommands: コマンドの実行 allowCommands: コマンドの実行
allowEdit: ファイルやディレクトリの編集、名前変更と削除 allowEdit: ファイルやディレクトリの編集、名前変更と削除
allowNew: ファイルとディレクトリの作成 allowNew: ファイルとディレクトリの作成
allowPublish: ポストとぺーじの発表 allowPublish: ポストとぺーじの発表
avoidChanges: "(変更を避けるために空白にしてください)" avoidChanges: "(変更を避けるために空白にしてください)"
changePassword: パスワードを変更 changePassword: パスワードを変更
commands: コマンド commands: コマンド
commandsHelp: "\ commandsHelp: "\
ここで、名前付きイベントに実行するコマンドを設定することができます。\ ここで、名前付きイベントに実行するコマンドを設定することができます。\
一行にコマンド一つを入力してください。\ 一行にコマンド一つを入力してください。\
イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\ イベントはファイルに関連する場合、例えばファイル保存の前にまたは後で、\
環境変数 FILE はファイルのパスに割り当てられます。" 環境変数 FILE はファイルのパスに割り当てられます。"
commandsUpdated: コマンドは更新されました! commandsUpdated: コマンドは更新されました!
customStylesheet: カスタムスタイルシ ート customStylesheet: カスタムスタイルシ ート
examples: examples:
globalSettings: グローバル設定 globalSettings: グローバル設定
language: 言語 language: 言語
lockPassword: 新しいパスワードを変更に禁止 lockPassword: 新しいパスワードを変更に禁止
newPassword: 新しいパスワード newPassword: 新しいパスワード
newPasswordConfirm: 新しいパスワードを確認します newPasswordConfirm: 新しいパスワードを確認します
newUser: 新しいユーザー newUser: 新しいユーザー
password: パスワード password: パスワード
passwordUpdated: パスワードは更新されました! passwordUpdated: パスワードは更新されました!
permissions: 権限 permissions: 権限
permissionsHelp: "\ permissionsHelp: "\
あなたはユーザーを管理者に設定し、または権限を個々に設定しできます。\ あなたはユーザーを管理者に設定し、または権限を個々に設定しできます。\
\"管理者\"を選択する場合、その他のすべての選択肢は自動的に設定されます。\ \"管理者\"を選択する場合、その他のすべての選択肢は自動的に設定されます。\
ユーザーの管理は管理者の権限として保留されました。" ユーザーの管理は管理者の権限として保留されました。"
profileSettings: プロファイル設定 profileSettings: プロファイル設定
ruleExample1: "\ ruleExample1: "\
各フォルダに名前はドットで始まるファイル(例えば、.git、.gitignore\ 各フォルダに名前はドットで始まるファイル(例えば、.git、.gitignore\
へのアクセスを制限します。" へのアクセスを制限します。"
ruleExample2: 範囲のルートパスに名前は Caddyfile のファイルへのアクセスを制限します。 ruleExample2: 範囲のルートパスに名前は Caddyfile のファイルへのアクセスを制限します。
rules: 規則 rules: 規則
rulesHelp1: "\ rulesHelp1: "\
ここに、あなたはこのユーザーの許可または拒否規則を設定できます。\ ここに、あなたはこのユーザーの許可または拒否規則を設定できます。\
ブロックされたファイルはリストに表示されません、それではアクセスも制限されます。\ ブロックされたファイルはリストに表示されません、それではアクセスも制限されます。\
正規表現(regex)のサポートと範囲に相対のパスが提供されています。" 正規表現(regex)のサポートと範囲に相対のパスが提供されています。"
rulesHelp2: "\ rulesHelp2: "\
一行に規則一つを入力してください、\ 一行に規則一つを入力してください、\
その間に規則はキーワード {0} や {1} で始める必要があります。\ その間に規則はキーワード {0} や {1} で始める必要があります。\
そして正規表現を使う場合、{2} と入力し、表現やパスを入力してください。" そして正規表現を使う場合、{2} と入力し、表現やパスを入力してください。"
scope: 範囲 scope: 範囲
settingsUpdated: 設定は更新されました! settingsUpdated: 設定は更新されました!
user: ユーザー user: ユーザー
userCommands: ユーザーのコマンド userCommands: ユーザーのコマンド
userCommandsHelp: "\ userCommandsHelp: "\
空白区切りの有効のコマンドのリストを指定してください。\ 空白区切りの有効のコマンドのリストを指定してください。\
例:" 例:"
userCreated: ユーザーは作成されました! userCreated: ユーザーは作成されました!
userDeleted: ユーザーは削除されました! userDeleted: ユーザーは削除されました!
userManagement: ユーザー管理 userManagement: ユーザー管理
username: ユーザー名 username: ユーザー名
users: ユーザー users: ユーザー
userUpdated: ユーザーは更新されました! userUpdated: ユーザーは更新されました!
sidebar: sidebar:
help: ヘルプ help: ヘルプ
logout: ログアウト logout: ログアウト
myFiles: 私のファイル myFiles: 私のファイル
newFile: 新しいファイルを作成 newFile: 新しいファイルを作成
newFolder: 新しいフォルダを作成 newFolder: 新しいフォルダを作成
settings: 設定 settings: 設定
siteSettings: サイト設定 siteSettings: サイト設定
hugoNew: Hugo New hugoNew: Hugo New
preview: プレビュー preview: プレビュー
search: search:
images: 画像 images: 画像
music: 音楽 music: 音楽
pdf: PDF pdf: PDF
pressToExecute: Enter を押して実行します。 pressToExecute: Enter を押して実行します。
pressToSearch: Enter を押して検索します。 pressToSearch: Enter を押して検索します。
search: 検索... search: 検索...
searchOrCommand: コマンドを検索または実行します。 searchOrCommand: コマンドを検索または実行します。
searchOrSupportedCommand: サポートしているコマンドを検索または実行します: searchOrSupportedCommand: サポートしているコマンドを検索または実行します:
type: キーワードを入力し、Enter を押して検索します。 type: キーワードを入力し、Enter を押して検索します。
types: 種類 types: 種類
video: ビデオ video: ビデオ
writeToSearch: ここにキーワードを入力してください writeToSearch: ここにキーワードを入力してください
languages: languages:
en: English en: English
fr: Français fr: Français
pt: Português pt: Português
ja: 日本語 ja: 日本語
zhCN: 中文 (简体) zhCN: 中文 (简体)
zhTW: 中文 (繁體) zhTW: 中文 (繁體)
time: time:
unit: 時間単位 unit: 時間単位
seconds: seconds:
minutes: minutes:
hours: 時間 hours: 時間
days: days:

View File

@ -1,198 +1,198 @@
permanent: 永久 permanent: 永久
buttons: buttons:
cancel: 取消 cancel: 取消
close: 关闭 close: 关闭
copy: 复制 copy: 复制
copyFile: 复制文件 copyFile: 复制文件
copyToClipboard: 复制到剪贴板 copyToClipboard: 复制到剪贴板
create: 创建 create: 创建
delete: 删除 delete: 删除
download: 下载 download: 下载
info: 信息 info: 信息
more: 更多 more: 更多
move: 移动 move: 移动
moveFile: 移动文件 moveFile: 移动文件
new: new:
next: 下一个 next: 下一个
ok: 确定 ok: 确定
replace: 替换 replace: 替换
previous: 上一个 previous: 上一个
rename: 重命名 rename: 重命名
reportIssue: 报告问题 reportIssue: 报告问题
save: 保存 save: 保存
search: 搜索 search: 搜索
select: 选择 select: 选择
share: 分享 share: 分享
publish: 发布 publish: 发布
selectMultiple: 选择多个 selectMultiple: 选择多个
schedule: 计划 schedule: 计划
switchView: 切换显示方式 switchView: 切换显示方式
toggleSidebar: 切换侧边栏 toggleSidebar: 切换侧边栏
update: 更新 update: 更新
upload: 上传 upload: 上传
permalink: 获取永久链接 permalink: 获取永久链接
success: success:
linkCopied: 链接已复制! linkCopied: 链接已复制!
errors: errors:
forbidden: 你被禁止访问。 forbidden: 你被禁止访问。
internal: 内部出现麻烦了。 internal: 内部出现麻烦了。
notFound: 找不到文件。 notFound: 找不到文件。
files: files:
folders: 文件夹 folders: 文件夹
files: 文件 files: 文件
body: Body body: Body
clear: 清空 clear: 清空
closePreview: 关闭预览 closePreview: 关闭预览
home: 主页 home: 主页
lastModified: 最后修改 lastModified: 最后修改
loading: 加载中... loading: 加载中...
lonely: 这里没有任何文件... lonely: 这里没有任何文件...
metadata: 元数据 metadata: 元数据
multipleSelectionEnabled: 多选模式已开启 multipleSelectionEnabled: 多选模式已开启
name: 名称 name: 名称
size: 大小 size: 大小
sortByName: 按名称排序 sortByName: 按名称排序
sortBySize: 按大小排序 sortBySize: 按大小排序
sortByLastModified: 按最后修改时间排序 sortByLastModified: 按最后修改时间排序
help: help:
click: 选择文件或目录 click: 选择文件或目录
ctrl: ctrl:
click: 选择多个文件或目录 click: 选择多个文件或目录
f: 打开搜索框 f: 打开搜索框
s: 保存文件或下载当前文件夹 s: 保存文件或下载当前文件夹
del: 删除所选的文件/文件夹 del: 删除所选的文件/文件夹
doubleClick: 打开文件/文件夹 doubleClick: 打开文件/文件夹
esc: 清除已选项或关闭提示信息 esc: 清除已选项或关闭提示信息
f1: 显示该帮助信息 f1: 显示该帮助信息
f2: 重命名文件/文件夹 f2: 重命名文件/文件夹
help: 帮助 help: 帮助
login: login:
password: 密码 password: 密码
submit: 登录 submit: 登录
username: 用户名 username: 用户名
wrongCredentials: 用户名或密码错误 wrongCredentials: 用户名或密码错误
prompts: prompts:
copy: 复制 copy: 复制
copyMessage: 请选择欲复制至的目录: copyMessage: 请选择欲复制至的目录:
currentlyNavigating: 当前目录: currentlyNavigating: 当前目录:
deleteMessageMultiple: 你确定要删除这 {count} 个文件吗? deleteMessageMultiple: 你确定要删除这 {count} 个文件吗?
deleteMessageSingle: 你确定要删除这个文件/文件夹吗? deleteMessageSingle: 你确定要删除这个文件/文件夹吗?
deleteTitle: 删除文件 deleteTitle: 删除文件
displayName: 名称: displayName: 名称:
download: 下载文件 download: 下载文件
downloadMessage: 请选择要下载的压缩格式。 downloadMessage: 请选择要下载的压缩格式。
error: 出了一点问题... error: 出了一点问题...
fileInfo: 文件信息 fileInfo: 文件信息
filesSelected: 已选择 {count} 个文件。 filesSelected: 已选择 {count} 个文件。
lastModified: 最后修改 lastModified: 最后修改
move: 移动 move: 移动
moveMessage: 请选择欲移动至的目录: moveMessage: 请选择欲移动至的目录:
newDir: 新建目录 newDir: 新建目录
newDirMessage: 请输入新目录的名称。 newDirMessage: 请输入新目录的名称。
newFile: 新建文件 newFile: 新建文件
newFileMessage: 请输入新文件的名称。 newFileMessage: 请输入新文件的名称。
numberDirs: 目录数 numberDirs: 目录数
numberFiles: 文件数 numberFiles: 文件数
replace: 替换 replace: 替换
replaceMessage: "\ replaceMessage: "\
您尝试上传的文件中有一个与现有文件的名称存在冲突。\ 您尝试上传的文件中有一个与现有文件的名称存在冲突。\
是否替换现有的同名文件?" 是否替换现有的同名文件?"
rename: 重命名 rename: 重命名
renameMessage: 请输入新名称,旧名称为: renameMessage: 请输入新名称,旧名称为:
show: 揭示 show: 揭示
size: 大小 size: 大小
schedule: 计划 schedule: 计划
scheduleMessage: 请选择发布这篇帖子的日期。 scheduleMessage: 请选择发布这篇帖子的日期。
newArchetype: 创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。 newArchetype: 创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。
settings: settings:
admin: 管理员 admin: 管理员
administrator: 管理员 administrator: 管理员
allowCommands: 执行命令(Linux 代码) allowCommands: 执行命令(Linux 代码)
allowEdit: 编辑、重命名或删除文件/目录 allowEdit: 编辑、重命名或删除文件/目录
allowNew: 创建新文件和目录 allowNew: 创建新文件和目录
allowPublish: 发布新的帖子与页面 allowPublish: 发布新的帖子与页面
avoidChanges: '(留空以避免更改)' avoidChanges: '(留空以避免更改)'
changePassword: 更改密码 changePassword: 更改密码
commands: 命令(linux 代码) commands: 命令(linux 代码)
commandsHelp: "\ commandsHelp: "\
在这里,您可以设置在指定事件下执行的命令,一行一条。\ 在这里,您可以设置在指定事件下执行的命令,一行一条。\
若事件与文件相关,如“在保存文件前”,\ 若事件与文件相关,如“在保存文件前”,\
则文件的路径会被赋值给环境变量 \"FILE\"。" 则文件的路径会被赋值给环境变量 \"FILE\"。"
commandsUpdated: 命令已更新! commandsUpdated: 命令已更新!
customStylesheet: 自定义样式表 customStylesheet: 自定义样式表
examples: 例子 examples: 例子
globalSettings: 全局设置 globalSettings: 全局设置
language: 语言 language: 语言
lockPassword: 禁止用户修改密码 lockPassword: 禁止用户修改密码
newPassword: 您的新密码 newPassword: 您的新密码
newPasswordConfirm: 重输一遍新密码 newPasswordConfirm: 重输一遍新密码
newUser: 新建用户 newUser: 新建用户
password: 密码 password: 密码
passwordUpdated: 密码已更新! passwordUpdated: 密码已更新!
permissions: 权限 permissions: 权限
permissionsHelp: "\ permissionsHelp: "\
您可以将该用户设置为管理员,也可以单独选择各项权限。\ 您可以将该用户设置为管理员,也可以单独选择各项权限。\
如果选择了“管理员”,则其他的选项会被自动勾上,\ 如果选择了“管理员”,则其他的选项会被自动勾上,\
同时该用户可以管理其他用户。" 同时该用户可以管理其他用户。"
profileSettings: 配置文件设置 profileSettings: 配置文件设置
ruleExample1: "\ ruleExample1: "\
阻止用户访问所有文件夹下任何以 . 开头的文件\ 阻止用户访问所有文件夹下任何以 . 开头的文件\
(隐藏文件, 例如: .git, .gitignore)。" (隐藏文件, 例如: .git, .gitignore)。"
ruleExample2: 阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。 ruleExample2: 阻止用户访问其目录范围的根目录下名为 Caddyfile 的文件。
rules: 规则 rules: 规则
rulesHelp1: "\ rulesHelp1: "\
您可以为该用户制定一组黑名单或白名单式的规则,\ 您可以为该用户制定一组黑名单或白名单式的规则,\
被屏蔽的文件将不会显示在列表中,用户也无权限访问,\ 被屏蔽的文件将不会显示在列表中,用户也无权限访问,\
支持相对于目录范围的路径。" 支持相对于目录范围的路径。"
rulesHelp2: "\ rulesHelp2: "\
每行一条规则,且必须以关键词 {0} 或 {1} 开头。\ 每行一条规则,且必须以关键词 {0} 或 {1} 开头。\
如要使用正则表达式,请在加上 {2} 之后再附上表达式或路径。" 如要使用正则表达式,请在加上 {2} 之后再附上表达式或路径。"
scope: 目录范围 scope: 目录范围
settingsUpdated: 设置已更新! settingsUpdated: 设置已更新!
user: 用户 user: 用户
userCommands: 用户命令(Linux 代码) userCommands: 用户命令(Linux 代码)
userCommandsHelp: "\ userCommandsHelp: "\
指定该用户可以执行的命令(Linux 代码),用空格分隔。\ 指定该用户可以执行的命令(Linux 代码),用空格分隔。\
例如:" 例如:"
userCreated: 用户已创建! userCreated: 用户已创建!
userDeleted: 用户已删除! userDeleted: 用户已删除!
userManagement: 用户管理 userManagement: 用户管理
username: 用户名 username: 用户名
users: 用户 users: 用户
userUpdated: 用户已更新! userUpdated: 用户已更新!
sidebar: sidebar:
help: 帮助 help: 帮助
logout: 登出 logout: 登出
myFiles: 我的文件 myFiles: 我的文件
newFile: 新建文件 newFile: 新建文件
newFolder: 新建文件夹 newFolder: 新建文件夹
settings: 设置 settings: 设置
siteSettings: 网站设置 siteSettings: 网站设置
hugoNew: Hugo New hugoNew: Hugo New
preview: 预览 preview: 预览
search: search:
images: 图像 images: 图像
music: 音乐 music: 音乐
pdf: PDF pdf: PDF
pressToExecute: 按回车键执行。 pressToExecute: 按回车键执行。
pressToSearch: 按回车键搜索。 pressToSearch: 按回车键搜索。
search: 搜索... search: 搜索...
searchOrCommand: 搜索或者执行命令(Linux 代码)... searchOrCommand: 搜索或者执行命令(Linux 代码)...
searchOrSupportedCommand: 搜索或使用您可以使用的命令(一次只能执行一个命令) searchOrSupportedCommand: 搜索或使用您可以使用的命令(一次只能执行一个命令)
type: 键入并按回车键进行搜索。 type: 键入并按回车键进行搜索。
types: 类型 types: 类型
video: 视频 video: 视频
writeToSearch: 请输入要搜索的内容 writeToSearch: 请输入要搜索的内容
languages: languages:
en: English en: English
fr: Français fr: Français
pt: Português pt: Português
ja: 日本語 ja: 日本語
zhCN: 中文 (简体) zhCN: 中文 (简体)
zhTW: 中文 (繁體) zhTW: 中文 (繁體)
time: time:
unit: 时间单位 unit: 时间单位
seconds: seconds:
minutes: 分钟 minutes: 分钟
hours: 小时 hours: 小时
days: days:

View File

@ -1,60 +1,60 @@
// Most of the code from this file comes from: // Most of the code from this file comes from:
// https://github.com/codemirror/CodeMirror/blob/master/addon/mode/loadmode.js // https://github.com/codemirror/CodeMirror/blob/master/addon/mode/loadmode.js
import * as CodeMirror from 'codemirror' import * as CodeMirror from 'codemirror'
import store from '@/store' import store from '@/store'
// Make CodeMirror available globally so the modes' can register themselves. // Make CodeMirror available globally so the modes' can register themselves.
window.CodeMirror = CodeMirror window.CodeMirror = CodeMirror
CodeMirror.modeURL = store.state.baseURL + '/static/js/codemirror/mode/%N/%N.js' CodeMirror.modeURL = store.state.baseURL + '/static/js/codemirror/mode/%N/%N.js'
var loading = {} var loading = {}
function splitCallback (cont, n) { function splitCallback (cont, n) {
var countDown = n var countDown = n
return function () { return function () {
if (--countDown === 0) cont() if (--countDown === 0) cont()
} }
} }
function ensureDeps (mode, cont) { function ensureDeps (mode, cont) {
var deps = CodeMirror.modes[mode].dependencies var deps = CodeMirror.modes[mode].dependencies
if (!deps) return cont() if (!deps) return cont()
var missing = [] var missing = []
for (var i = 0; i < deps.length; ++i) { for (var i = 0; i < deps.length; ++i) {
if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i]) if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i])
} }
if (!missing.length) return cont() if (!missing.length) return cont()
var split = splitCallback(cont, missing.length) var split = splitCallback(cont, missing.length)
for (i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split) for (i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split)
} }
CodeMirror.requireMode = function (mode, cont) { CodeMirror.requireMode = function (mode, cont) {
if (typeof mode !== 'string') mode = mode.name if (typeof mode !== 'string') mode = mode.name
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont) if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont)
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont) if (loading.hasOwnProperty(mode)) return loading[mode].push(cont)
var file = CodeMirror.modeURL.replace(/%N/g, mode) var file = CodeMirror.modeURL.replace(/%N/g, mode)
var script = document.createElement('script') var script = document.createElement('script')
script.src = file script.src = file
var others = document.getElementsByTagName('script')[0] var others = document.getElementsByTagName('script')[0]
var list = loading[mode] = [cont] var list = loading[mode] = [cont]
CodeMirror.on(script, 'load', function () { CodeMirror.on(script, 'load', function () {
ensureDeps(mode, function () { ensureDeps(mode, function () {
for (var i = 0; i < list.length; ++i) list[i]() for (var i = 0; i < list.length; ++i) list[i]()
}) })
}) })
others.parentNode.insertBefore(script, others) others.parentNode.insertBefore(script, others)
} }
CodeMirror.autoLoadMode = function (instance, mode) { CodeMirror.autoLoadMode = function (instance, mode) {
if (CodeMirror.modes.hasOwnProperty(mode)) return if (CodeMirror.modes.hasOwnProperty(mode)) return
CodeMirror.requireMode(mode, function () { CodeMirror.requireMode(mode, function () {
instance.setOption('mode', mode) instance.setOption('mode', mode)
}) })
} }
export default CodeMirror export default CodeMirror

View File

@ -1,4 +1,4 @@
export default function (name) { export default function (name) {
let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$') let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$')
return document.cookie.replace(re, '$1') return document.cookie.replace(re, '$1')
} }

View File

@ -1,28 +1,28 @@
export default function getRule (rules) { export default function getRule (rules) {
for (let i = 0; i < rules.length; i++) { for (let i = 0; i < rules.length; i++) {
rules[i] = rules[i].toLowerCase() rules[i] = rules[i].toLowerCase()
} }
let result = null let result = null
let find = Array.prototype.find let find = Array.prototype.find
find.call(document.styleSheets, styleSheet => { find.call(document.styleSheets, styleSheet => {
result = find.call(styleSheet.cssRules, cssRule => { result = find.call(styleSheet.cssRules, cssRule => {
let found = false let found = false
if (cssRule instanceof window.CSSStyleRule) { if (cssRule instanceof window.CSSStyleRule) {
for (let i = 0; i < rules.length; i++) { for (let i = 0; i < rules.length; i++) {
if (cssRule.selectorText.toLowerCase() === rules[i]) { if (cssRule.selectorText.toLowerCase() === rules[i]) {
found = true found = true
} }
} }
} }
return found return found
}) })
return result != null return result != null
}) })
return result return result
} }

View File

@ -1,12 +1,12 @@
function removeLastDir (url) { function removeLastDir (url) {
var arr = url.split('/') var arr = url.split('/')
if (arr.pop() === '') { if (arr.pop() === '') {
arr.pop() arr.pop()
} }
return arr.join('/') return arr.join('/')
} }
export default { export default {
removeLastDir: removeLastDir removeLastDir: removeLastDir
} }

View File

@ -1,231 +1,231 @@
<template> <template>
<div> <div>
<div id="breadcrumbs"> <div id="breadcrumbs">
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')"> <router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
<i class="material-icons">home</i> <i class="material-icons">home</i>
</router-link> </router-link>
<span v-for="link in breadcrumbs" :key="link.name"> <span v-for="link in breadcrumbs" :key="link.name">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span> <span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<router-link :to="link.url">{{ link.name }}</router-link> <router-link :to="link.url">{{ link.name }}</router-link>
</span> </span>
</div> </div>
<div v-if="error"> <div v-if="error">
<not-found v-if="error.message === '404'"></not-found> <not-found v-if="error.message === '404'"></not-found>
<forbidden v-else-if="error.message === '403'"></forbidden> <forbidden v-else-if="error.message === '403'"></forbidden>
<internal-error v-else></internal-error> <internal-error v-else></internal-error>
</div> </div>
<editor v-else-if="isEditor"></editor> <editor v-else-if="isEditor"></editor>
<listing :class="{ multiple }" v-else-if="isListing"></listing> <listing :class="{ multiple }" v-else-if="isListing"></listing>
<preview v-else-if="isPreview"></preview> <preview v-else-if="isPreview"></preview>
<div v-else> <div v-else>
<h2 class="message"> <h2 class="message">
<span>{{ $t('files.loading') }}</span> <span>{{ $t('files.loading') }}</span>
</h2> </h2>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Forbidden from './errors/403' import Forbidden from './errors/403'
import NotFound from './errors/404' import NotFound from './errors/404'
import InternalError from './errors/500' import InternalError from './errors/500'
import Preview from '@/components/files/Preview' import Preview from '@/components/files/Preview'
import Listing from '@/components/files/Listing' import Listing from '@/components/files/Listing'
import Editor from '@/components/files/Editor' import Editor from '@/components/files/Editor'
import * as api from '@/utils/api' import * as api from '@/utils/api'
import { mapGetters, mapState, mapMutations } from 'vuex' import { mapGetters, mapState, mapMutations } from 'vuex'
export default { export default {
name: 'files', name: 'files',
components: { components: {
Forbidden, Forbidden,
NotFound, NotFound,
InternalError, InternalError,
Preview, Preview,
Listing, Listing,
Editor Editor
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
'selectedCount' 'selectedCount'
]), ]),
...mapState([ ...mapState([
'req', 'req',
'user', 'user',
'reload', 'reload',
'multiple', 'multiple',
'loading' 'loading'
]), ]),
isListing () { isListing () {
return this.req.kind === 'listing' && !this.loading return this.req.kind === 'listing' && !this.loading
}, },
isPreview () { isPreview () {
return this.req.kind === 'preview' && !this.loading return this.req.kind === 'preview' && !this.loading
}, },
isEditor () { isEditor () {
return this.req.kind === 'editor' && !this.loading return this.req.kind === 'editor' && !this.loading
}, },
breadcrumbs () { breadcrumbs () {
let parts = this.$route.path.split('/') let parts = this.$route.path.split('/')
if (parts[0] === '') { if (parts[0] === '') {
parts.shift() parts.shift()
} }
if (parts[parts.length - 1] === '') { if (parts[parts.length - 1] === '') {
parts.pop() parts.pop()
} }
let breadcrumbs = [] let breadcrumbs = []
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (i === 0) { if (i === 0) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/' + parts[i] + '/' }) breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/' + parts[i] + '/' })
} else { } else {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' }) breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
} }
} }
breadcrumbs.shift() breadcrumbs.shift()
if (breadcrumbs.length > 3) { if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) { while (breadcrumbs.length !== 4) {
breadcrumbs.shift() breadcrumbs.shift()
} }
breadcrumbs[0].name = '...' breadcrumbs[0].name = '...'
} }
return breadcrumbs return breadcrumbs
} }
}, },
data: function () { data: function () {
return { return {
error: null error: null
} }
}, },
created () { created () {
this.fetchData() this.fetchData()
}, },
watch: { watch: {
'$route': 'fetchData', '$route': 'fetchData',
'reload': function () { 'reload': function () {
this.fetchData() this.fetchData()
} }
}, },
mounted () { mounted () {
window.addEventListener('keydown', this.keyEvent) window.addEventListener('keydown', this.keyEvent)
window.addEventListener('scroll', this.scroll) window.addEventListener('scroll', this.scroll)
}, },
beforeDestroy () { beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent) window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('scroll', this.scroll) window.removeEventListener('scroll', this.scroll)
}, },
destroyed () { destroyed () {
this.$store.commit('updateRequest', {}) this.$store.commit('updateRequest', {})
}, },
methods: { methods: {
...mapMutations([ 'setLoading' ]), ...mapMutations([ 'setLoading' ]),
fetchData () { fetchData () {
// Reset view information. // Reset view information.
this.$store.commit('setReload', false) this.$store.commit('setReload', false)
this.$store.commit('resetSelected') this.$store.commit('resetSelected')
this.$store.commit('multiple', false) this.$store.commit('multiple', false)
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
// Set loading to true and reset the error. // Set loading to true and reset the error.
this.setLoading(true) this.setLoading(true)
this.error = null this.error = null
let url = this.$route.path let url = this.$route.path
if (url === '') url = '/' if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url if (url[0] !== '/') url = '/' + url
api.fetch(url) api.fetch(url)
.then((req) => { .then((req) => {
if (!url.endsWith('/') && req.url.endsWith('/')) { if (!url.endsWith('/') && req.url.endsWith('/')) {
window.history.replaceState(window.history.state, document.title, window.location.pathname + '/') window.history.replaceState(window.history.state, document.title, window.location.pathname + '/')
} }
this.$store.commit('updateRequest', req) this.$store.commit('updateRequest', req)
document.title = req.name document.title = req.name
this.setLoading(false) this.setLoading(false)
}) })
.catch(error => { .catch(error => {
this.setLoading(false) this.setLoading(false)
this.error = error this.error = error
}) })
}, },
keyEvent (event) { keyEvent (event) {
// Esc! // Esc!
if (event.keyCode === 27) { if (event.keyCode === 27) {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
// If we're on a listing, unselect all // If we're on a listing, unselect all
// files and folders. // files and folders.
if (this.req.kind === 'listing') { if (this.req.kind === 'listing') {
this.$store.commit('resetSelected') this.$store.commit('resetSelected')
} }
} }
// Del! // Del!
if (event.keyCode === 46) { if (event.keyCode === 46) {
if (this.req.kind === 'editor' || if (this.req.kind === 'editor' ||
this.$route.name !== 'Files' || this.$route.name !== 'Files' ||
this.loading || this.loading ||
!this.user.allowEdit || !this.user.allowEdit ||
(this.req.kind === 'listing' && this.selectedCount === 0)) return (this.req.kind === 'listing' && this.selectedCount === 0)) return
this.$store.commit('showHover', 'delete') this.$store.commit('showHover', 'delete')
} }
// F1! // F1!
if (event.keyCode === 112) { if (event.keyCode === 112) {
event.preventDefault() event.preventDefault()
this.$store.commit('showHover', 'help') this.$store.commit('showHover', 'help')
} }
// F2! // F2!
if (event.keyCode === 113) { if (event.keyCode === 113) {
if (this.req.kind === 'editor' || if (this.req.kind === 'editor' ||
this.$route.name !== 'Files' || this.$route.name !== 'Files' ||
this.loading || this.loading ||
!this.user.allowEdit || !this.user.allowEdit ||
(this.req.kind === 'listing' && this.selectedCount === 0) || (this.req.kind === 'listing' && this.selectedCount === 0) ||
(this.req.kind === 'listing' && this.selectedCount > 1)) return (this.req.kind === 'listing' && this.selectedCount > 1)) return
this.$store.commit('showHover', 'rename') this.$store.commit('showHover', 'rename')
} }
// CTRL + S // CTRL + S
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
if (String.fromCharCode(event.which).toLowerCase() === 's') { if (String.fromCharCode(event.which).toLowerCase() === 's') {
event.preventDefault() event.preventDefault()
if (this.req.kind !== 'editor') { if (this.req.kind !== 'editor') {
document.getElementById('download-button').click() document.getElementById('download-button').click()
} }
} }
} }
}, },
scroll (event) { scroll (event) {
if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return if (this.req.kind !== 'listing' || this.$store.state.req.display === 'mosaic') return
let top = 112 - window.scrollY let top = 112 - window.scrollY
if (top < 64) { if (top < 64) {
top = 64 top = 64
} }
document.querySelector('#listing.list .item.header').style.top = top + 'px' document.querySelector('#listing.list .item.header').style.top = top + 'px'
}, },
openSidebar () { openSidebar () {
this.$store.commit('showHover', 'sidebar') this.$store.commit('showHover', 'sidebar')
}, },
openSearch () { openSearch () {
this.$store.commit('showHover', 'search') this.$store.commit('showHover', 'search')
} }
} }
} }
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<h2 class="message"> <h2 class="message">
<i class="material-icons">error</i> <i class="material-icons">error</i>
<span>{{ $t('errors.forbidden') }}</span> <span>{{ $t('errors.forbidden') }}</span>
</h2> </h2>
</div> </div>
</template> </template>
<script> <script>
export default {name: 'forbidden'} export default {name: 'forbidden'}
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<h2 class="message"> <h2 class="message">
<i class="material-icons">gps_off</i> <i class="material-icons">gps_off</i>
<span>{{ $t('errors.notFound') }}</span> <span>{{ $t('errors.notFound') }}</span>
</h2> </h2>
</div> </div>
</template> </template>
<script> <script>
export default {name: 'not-found'} export default {name: 'not-found'}
</script> </script>

View File

@ -1,13 +1,13 @@
<template> <template>
<div> <div>
<h2 class="message"> <h2 class="message">
<i class="material-icons">error_outline</i> <i class="material-icons">error_outline</i>
<span>{{ $t('errors.internal') }}</span> <span>{{ $t('errors.internal') }}</span>
</h2> </h2>
</div> </div>
</template> </template>
<script> <script>
export default {name: 'internal-error'} export default {name: 'internal-error'}
</script> </script>

View File

@ -1,20 +1,20 @@
{ {
"name": "File Manager", "name": "File Manager",
"short_name": "File Manager", "short_name": "File Manager",
"icons": [ "icons": [
{ {
"src": "{{ .BaseURL }}/static/img/icons/android-chrome-192x192.png", "src": "{{ .BaseURL }}/static/img/icons/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "{{ .BaseURL }}/static/img/icons/android-chrome-512x512.png", "src": "{{ .BaseURL }}/static/img/icons/android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
], ],
"start_url": "{{ .BaseURL }}/", "start_url": "{{ .BaseURL }}/",
"display": "standalone", "display": "standalone",
"background_color": "#ffffff", "background_color": "#ffffff",
"theme_color": "#2979ff" "theme_color": "#2979ff"
} }

View File

@ -1,50 +1,50 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>File Manager</title> <title>File Manager</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]--> <!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json"> <link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff"> <meta name="theme-color" content="#2979ff">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets"> <meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png"> <link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png"> <meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff"> <meta name="msapplication-TileColor" content="#2979ff">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
<style> <style>
* { * {
box-sizing: border-box box-sizing: border-box
} }
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
color: #6f6f6f; color: #6f6f6f;
background: #f8f8f8; background: #f8f8f8;
} }
body > div { body > div {
text-align: center; text-align: center;
position: absolute; position: absolute;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
top: 50%; top: 50%;
left: 50%; left: 50%;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff; background: #fff;
display: block; display: block;
border-radius: 0.2em; border-radius: 0.2em;
padding: 2em 3em; padding: 2em 3em;
} }
body > a * { body > a * {
margin: 0; margin: 0;
} }
</style> </style>
</head> </head>
<body> <body>
<div><h1>404 Not Found</h1></div> <div><h1>404 Not Found</h1></div>
</body> </body>
</html> </html>

View File

@ -1,85 +1,85 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>{{ .File.Name }}</title> <title>{{ .File.Name }}</title>
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]--> <!--[if IE]><link rel="shortcut icon" href="{{ .BaseURL }}/static/img/icons/favicon.ico"><![endif]-->
<link rel="manifest" href="{{ .BaseURL }}/static/manifest.json"> <link rel="manifest" href="{{ .BaseURL }}/static/manifest.json">
<meta name="theme-color" content="#2979ff"> <meta name="theme-color" content="#2979ff">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets"> <meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png"> <link rel="apple-touch-icon" href="{{ .BaseURL }}/static/img/icons/apple-touch-icon-152x152.png">
<meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png"> <meta name="msapplication-TileImage" content="{{ .BaseURL }}/static/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff"> <meta name="msapplication-TileColor" content="#2979ff">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css">
<style> <style>
* { * {
box-sizing: border-box box-sizing: border-box
} }
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
color: #6f6f6f; color: #6f6f6f;
background: #f8f8f8; background: #f8f8f8;
} }
a { a {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
body > a { body > a {
text-align: center; text-align: center;
position: absolute; position: absolute;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
top: 50%; top: 50%;
left: 50%; left: 50%;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff; background: #fff;
display: block; display: block;
border-radius: 0.2em; border-radius: 0.2em;
width: 90%; width: 90%;
max-width: 25em; max-width: 25em;
} }
body > a > div:first-child { body > a > div:first-child {
width: 100%; width: 100%;
padding: 1em; padding: 1em;
cursor: pointer; cursor: pointer;
background: #ffffff; background: #ffffff;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.05); border-bottom: 1px solid rgba(0, 0, 0, 0.05);
} }
body > a > div:last-child { body > a > div:last-child {
padding: 2em 3em; padding: 2em 3em;
} }
body > a * { body > a * {
margin: 0; margin: 0;
} }
body > a h1 { body > a h1 {
margin-top: .2em; margin-top: .2em;
} }
</style> </style>
</head> </head>
<body> <body>
<a href="?dl=1"> <a href="?dl=1">
<div>Download {{ if .File.IsDir }}Folder{{ else }}File{{ end }}</div> <div>Download {{ if .File.IsDir }}Folder{{ else }}File{{ end }}</div>
<div> <div>
{{ if .File.IsDir -}} {{ if .File.IsDir -}}
<svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg"> <svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/> <path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none"/>
</svg> </svg>
{{ else -}} {{ else -}}
<svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg"> <svg fill="#40c4ff" height="150" viewBox="0 0 24 24" width="150" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/> <path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none"/>
</svg> </svg>
{{ end -}} {{ end -}}
<h1>{{ .File.Name }}</h1> <h1>{{ .File.Name }}</h1>
</div> </div>
</a> </a>
</body> </body>
</html> </html>

View File

@ -1,26 +1,26 @@
package bolt package bolt
import ( import (
"github.com/asdine/storm" "github.com/asdine/storm"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
) )
// ConfigStore is a configuration store. // ConfigStore is a configuration store.
type ConfigStore struct { type ConfigStore struct {
DB *storm.DB DB *storm.DB
} }
// Get gets a configuration from the database to an interface. // Get gets a configuration from the database to an interface.
func (c ConfigStore) Get(name string, to interface{}) error { func (c ConfigStore) Get(name string, to interface{}) error {
err := c.DB.Get("config", name, to) err := c.DB.Get("config", name, to)
if err == storm.ErrNotFound { if err == storm.ErrNotFound {
return fm.ErrNotExist return fm.ErrNotExist
} }
return err return err
} }
// Save saves a configuration from an interface to the database. // Save saves a configuration from an interface to the database.
func (c ConfigStore) Save(name string, from interface{}) error { func (c ConfigStore) Save(name string, from interface{}) error {
return c.DB.Set("config", name, from) return c.DB.Set("config", name, from)
} }

View File

@ -1,90 +1,90 @@
package bolt package bolt
import ( import (
"reflect" "reflect"
"github.com/asdine/storm" "github.com/asdine/storm"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
) )
// UsersStore is a users store. // UsersStore is a users store.
type UsersStore struct { type UsersStore struct {
DB *storm.DB DB *storm.DB
} }
// Get gets a user with a certain id from the database. // Get gets a user with a certain id from the database.
func (u UsersStore) Get(id int, builder fm.FSBuilder) (*fm.User, error) { func (u UsersStore) Get(id int, builder fm.FSBuilder) (*fm.User, error) {
var us fm.User var us fm.User
err := u.DB.One("ID", id, &us) err := u.DB.One("ID", id, &us)
if err == storm.ErrNotFound { if err == storm.ErrNotFound {
return nil, fm.ErrNotExist return nil, fm.ErrNotExist
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
us.FileSystem = builder(us.Scope) us.FileSystem = builder(us.Scope)
return &us, nil return &us, nil
} }
// GetByUsername gets a user with a certain username from the database. // GetByUsername gets a user with a certain username from the database.
func (u UsersStore) GetByUsername(username string, builder fm.FSBuilder) (*fm.User, error) { func (u UsersStore) GetByUsername(username string, builder fm.FSBuilder) (*fm.User, error) {
var us fm.User var us fm.User
err := u.DB.One("Username", username, &us) err := u.DB.One("Username", username, &us)
if err == storm.ErrNotFound { if err == storm.ErrNotFound {
return nil, fm.ErrNotExist return nil, fm.ErrNotExist
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
us.FileSystem = builder(us.Scope) us.FileSystem = builder(us.Scope)
return &us, nil return &us, nil
} }
// Gets gets all the users from the database. // Gets gets all the users from the database.
func (u UsersStore) Gets(builder fm.FSBuilder) ([]*fm.User, error) { func (u UsersStore) Gets(builder fm.FSBuilder) ([]*fm.User, error) {
var us []*fm.User var us []*fm.User
err := u.DB.All(&us) err := u.DB.All(&us)
if err == storm.ErrNotFound { if err == storm.ErrNotFound {
return nil, fm.ErrNotExist return nil, fm.ErrNotExist
} }
if err != nil { if err != nil {
return us, err return us, err
} }
for _, user := range us { for _, user := range us {
user.FileSystem = builder(user.Scope) user.FileSystem = builder(user.Scope)
} }
return us, err return us, err
} }
// Update updates the whole user object or only certain fields. // Update updates the whole user object or only certain fields.
func (u UsersStore) Update(us *fm.User, fields ...string) error { func (u UsersStore) Update(us *fm.User, fields ...string) error {
if len(fields) == 0 { if len(fields) == 0 {
return u.Save(us) return u.Save(us)
} }
for _, field := range fields { for _, field := range fields {
val := reflect.ValueOf(us).Elem().FieldByName(field).Interface() val := reflect.ValueOf(us).Elem().FieldByName(field).Interface()
if err := u.DB.UpdateField(us, field, val); err != nil { if err := u.DB.UpdateField(us, field, val); err != nil {
return err return err
} }
} }
return nil return nil
} }
// Save saves a user to the database. // Save saves a user to the database.
func (u UsersStore) Save(us *fm.User) error { func (u UsersStore) Save(us *fm.User) error {
return u.DB.Save(us) return u.DB.Save(us)
} }
// Delete deletes a user from the database. // Delete deletes a user from the database.
func (u UsersStore) Delete(id int) error { func (u UsersStore) Delete(id int) error {
return u.DB.DeleteStruct(&fm.User{ID: id}) return u.DB.DeleteStruct(&fm.User{ID: id})
} }

View File

@ -1,13 +1,13 @@
#!/bin/bash #!/bin/bash
# Install rice tool if not present # Install rice tool if not present
if ! [ -x "$(command -v rice)" ]; then if ! [ -x "$(command -v rice)" ]; then
go get github.com/GeertJohan/go.rice/rice go get github.com/GeertJohan/go.rice/rice
fi fi
# Clean the dist folder and build the assets # Clean the dist folder and build the assets
rm -rf assets/dist rm -rf assets/dist
npm run build npm run build
# Embed the assets using rice # Embed the assets using rice
rice embed-go rice embed-go

View File

@ -1,55 +1,55 @@
// Package filemanager provides middleware for managing files in a directory // Package filemanager provides middleware for managing files in a directory
// when directory path is requested instead of a specific file. Based on browse // when directory path is requested instead of a specific file. Based on browse
// middleware. // middleware.
package filemanager package filemanager
import ( import (
"net/http" "net/http"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
func init() { func init() {
caddy.RegisterPlugin("filemanager", caddy.Plugin{ caddy.RegisterPlugin("filemanager", caddy.Plugin{
ServerType: "http", ServerType: "http",
Action: setup, Action: setup,
}) })
} }
type plugin struct { type plugin struct {
Next httpserver.Handler Next httpserver.Handler
Configs []*filemanager.FileManager Configs []*filemanager.FileManager
} }
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. // 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) { func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs { for i := range f.Configs {
// Checks if this Path should be handled by File Manager. // Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue continue
} }
h.Handler(f.Configs[i]).ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return f.Next.ServeHTTP(w, r) return f.Next.ServeHTTP(w, r)
} }
// setup configures a new FileManager middleware instance. // setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "") configs, err := parser.Parse(c, "")
if err != nil { if err != nil {
return err return err
} }
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next} return plugin{Configs: configs, Next: next}
}) })
return nil return nil
} }

View File

@ -1,52 +1,52 @@
package hugo package hugo
import ( import (
"net/http" "net/http"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
func init() { func init() {
caddy.RegisterPlugin("hugo", caddy.Plugin{ caddy.RegisterPlugin("hugo", caddy.Plugin{
ServerType: "http", ServerType: "http",
Action: setup, Action: setup,
}) })
} }
type plugin struct { type plugin struct {
Next httpserver.Handler Next httpserver.Handler
Configs []*filemanager.FileManager Configs []*filemanager.FileManager
} }
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. // 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) { func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs { for i := range f.Configs {
// Checks if this Path should be handled by File Manager. // Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue continue
} }
h.Handler(f.Configs[i]).ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return f.Next.ServeHTTP(w, r) return f.Next.ServeHTTP(w, r)
} }
// setup configures a new FileManager middleware instance. // setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "hugo") configs, err := parser.Parse(c, "hugo")
if err != nil { if err != nil {
return err return err
} }
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next} return plugin{Configs: configs, Next: next}
}) })
return nil return nil
} }

View File

@ -1,52 +1,52 @@
package jekyll package jekyll
import ( import (
"net/http" "net/http"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/caddy/parser" "github.com/hacdias/filemanager/caddy/parser"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
func init() { func init() {
caddy.RegisterPlugin("jekyll", caddy.Plugin{ caddy.RegisterPlugin("jekyll", caddy.Plugin{
ServerType: "http", ServerType: "http",
Action: setup, Action: setup,
}) })
} }
type plugin struct { type plugin struct {
Next httpserver.Handler Next httpserver.Handler
Configs []*filemanager.FileManager Configs []*filemanager.FileManager
} }
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met. // 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) { func (f plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for i := range f.Configs { for i := range f.Configs {
// Checks if this Path should be handled by File Manager. // Checks if this Path should be handled by File Manager.
if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) { if !httpserver.Path(r.URL.Path).Matches(f.Configs[i].BaseURL) {
continue continue
} }
h.Handler(f.Configs[i]).ServeHTTP(w, r) h.Handler(f.Configs[i]).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return f.Next.ServeHTTP(w, r) return f.Next.ServeHTTP(w, r)
} }
// setup configures a new FileManager middleware instance. // setup configures a new FileManager middleware instance.
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
configs, err := parser.Parse(c, "jekyll") configs, err := parser.Parse(c, "jekyll")
if err != nil { if err != nil {
return err return err
} }
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return plugin{Configs: configs, Next: next} return plugin{Configs: configs, Next: next}
}) })
return nil return nil
} }

View File

@ -1,294 +1,294 @@
package parser package parser
import ( import (
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/asdine/storm" "github.com/asdine/storm"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt" "github.com/hacdias/filemanager/bolt"
"github.com/hacdias/filemanager/staticgen" "github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
var databases = map[string]*storm.DB{} var databases = map[string]*storm.DB{}
// Parse ... // Parse ...
func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) { func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, error) {
var ( var (
configs []*filemanager.FileManager configs []*filemanager.FileManager
err error err error
) )
for c.Next() { for c.Next() {
u := &filemanager.User{ u := &filemanager.User{
Locale: "en", Locale: "en",
AllowCommands: true, AllowCommands: true,
AllowEdit: true, AllowEdit: true,
AllowNew: true, AllowNew: true,
AllowPublish: true, AllowPublish: true,
Commands: []string{"git", "svn", "hg"}, Commands: []string{"git", "svn", "hg"},
CSS: "", CSS: "",
Rules: []*filemanager.Rule{{ Rules: []*filemanager.Rule{{
Regex: true, Regex: true,
Allow: false, Allow: false,
Regexp: &filemanager.Regexp{Raw: "\\/\\..+"}, Regexp: &filemanager.Regexp{Raw: "\\/\\..+"},
}}, }},
} }
baseURL := "/" baseURL := "/"
scope := "." scope := "."
database := "" database := ""
noAuth := false noAuth := false
reCaptchaKey := "" reCaptchaKey := ""
reCaptchaSecret := "" reCaptchaSecret := ""
if plugin != "" { if plugin != "" {
baseURL = "/admin" baseURL = "/admin"
} }
// Get the baseURL and scope // Get the baseURL and scope
args := c.RemainingArgs() args := c.RemainingArgs()
if plugin == "" { if plugin == "" {
if len(args) >= 1 { if len(args) >= 1 {
baseURL = args[0] baseURL = args[0]
} }
if len(args) > 1 { if len(args) > 1 {
scope = args[1] scope = args[1]
} }
} else { } else {
if len(args) >= 1 { if len(args) >= 1 {
scope = args[0] scope = args[0]
} }
if len(args) > 1 { if len(args) > 1 {
baseURL = args[1] baseURL = args[1]
} }
} }
for c.NextBlock() { for c.NextBlock() {
switch c.Val() { switch c.Val() {
case "database": case "database":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
database = c.Val() database = c.Val()
case "locale": case "locale":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
u.Locale = c.Val() u.Locale = c.Val()
case "allow_commands": case "allow_commands":
if !c.NextArg() { if !c.NextArg() {
u.AllowCommands = true u.AllowCommands = true
continue continue
} }
u.AllowCommands, err = strconv.ParseBool(c.Val()) u.AllowCommands, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "allow_edit": case "allow_edit":
if !c.NextArg() { if !c.NextArg() {
u.AllowEdit = true u.AllowEdit = true
continue continue
} }
u.AllowEdit, err = strconv.ParseBool(c.Val()) u.AllowEdit, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "allow_new": case "allow_new":
if !c.NextArg() { if !c.NextArg() {
u.AllowNew = true u.AllowNew = true
continue continue
} }
u.AllowNew, err = strconv.ParseBool(c.Val()) u.AllowNew, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "allow_publish": case "allow_publish":
if !c.NextArg() { if !c.NextArg() {
u.AllowPublish = true u.AllowPublish = true
continue continue
} }
u.AllowPublish, err = strconv.ParseBool(c.Val()) u.AllowPublish, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "commands": case "commands":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
u.Commands = strings.Split(c.Val(), " ") u.Commands = strings.Split(c.Val(), " ")
case "css": case "css":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
file := c.Val() file := c.Val()
css, err := ioutil.ReadFile(file) css, err := ioutil.ReadFile(file)
if err != nil { if err != nil {
return nil, err return nil, err
} }
u.CSS = string(css) u.CSS = string(css)
case "view_mode": case "view_mode":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
u.ViewMode = c.Val() u.ViewMode = c.Val()
if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode { if u.ViewMode != filemanager.MosaicViewMode && u.ViewMode != filemanager.ListViewMode {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
case "recaptcha_key": case "recaptcha_key":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
reCaptchaKey = c.Val() reCaptchaKey = c.Val()
case "recaptcha_secret": case "recaptcha_secret":
if !c.NextArg() { if !c.NextArg() {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
reCaptchaSecret = c.Val() reCaptchaSecret = c.Val()
case "no_auth": case "no_auth":
if !c.NextArg() { if !c.NextArg() {
noAuth = true noAuth = true
continue continue
} }
noAuth, err = strconv.ParseBool(c.Val()) noAuth, err = strconv.ParseBool(c.Val())
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
} }
caddyConf := httpserver.GetConfig(c) caddyConf := httpserver.GetConfig(c)
path := filepath.Join(caddy.AssetsPath(), "filemanager") path := filepath.Join(caddy.AssetsPath(), "filemanager")
err := os.MkdirAll(path, 0700) err := os.MkdirAll(path, 0700)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// if there is a database path and it is not absolute, // if there is a database path and it is not absolute,
// it will be relative to Caddy folder. // it will be relative to Caddy folder.
if !filepath.IsAbs(database) && database != "" { if !filepath.IsAbs(database) && database != "" {
database = filepath.Join(path, database) database = filepath.Join(path, database)
} }
// If there is no database path on the settings, // If there is no database path on the settings,
// store one in .caddy/filemanager/name.db. // store one in .caddy/filemanager/name.db.
if database == "" { if database == "" {
// The name of the database is the hashed value of a string composed // 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 // by the host, address path and the baseurl of this File Manager
// instance. // instance.
hasher := md5.New() hasher := md5.New()
hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL)) hasher.Write([]byte(caddyConf.Addr.Host + caddyConf.Addr.Path + baseURL))
sha := hex.EncodeToString(hasher.Sum(nil)) sha := hex.EncodeToString(hasher.Sum(nil))
database = filepath.Join(path, sha+".db") database = filepath.Join(path, sha+".db")
fmt.Println("[WARNING] A database is going to be created for your File Manager instance at " + database + 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") ". It is highly recommended that you set the 'database' option to '" + sha + ".db'\n")
} }
u.Scope = scope u.Scope = scope
u.FileSystem = fileutils.Dir(scope) u.FileSystem = fileutils.Dir(scope)
var db *storm.DB var db *storm.DB
if stored, ok := databases[database]; ok { if stored, ok := databases[database]; ok {
db = stored db = stored
} else { } else {
db, err = storm.Open(database) db, err = storm.Open(database)
databases[database] = db databases[database] = db
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
m := &filemanager.FileManager{ m := &filemanager.FileManager{
NoAuth: noAuth, NoAuth: noAuth,
BaseURL: "", BaseURL: "",
PrefixURL: "", PrefixURL: "",
ReCaptchaKey: reCaptchaKey, ReCaptchaKey: reCaptchaKey,
ReCaptchaSecret: reCaptchaSecret, ReCaptchaSecret: reCaptchaSecret,
DefaultUser: u, DefaultUser: u,
Store: &filemanager.Store{ Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db}, Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db}, Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db}, Share: bolt.ShareStore{DB: db},
}, },
NewFS: func(scope string) filemanager.FileSystem { NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope) return fileutils.Dir(scope)
}, },
} }
err = m.Setup() err = m.Setup()
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch plugin { switch plugin {
case "hugo": case "hugo":
// Initialize the default settings for Hugo. // Initialize the default settings for Hugo.
hugo := &staticgen.Hugo{ hugo := &staticgen.Hugo{
Root: scope, Root: scope,
Public: filepath.Join(scope, "public"), Public: filepath.Join(scope, "public"),
Args: []string{}, Args: []string{},
CleanPublic: true, CleanPublic: true,
} }
// Attaches Hugo plugin to this file manager instance. // Attaches Hugo plugin to this file manager instance.
err = m.Attach(hugo) err = m.Attach(hugo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "jekyll": case "jekyll":
// Initialize the default settings for Jekyll. // Initialize the default settings for Jekyll.
jekyll := &staticgen.Jekyll{ jekyll := &staticgen.Jekyll{
Root: scope, Root: scope,
Public: filepath.Join(scope, "_site"), Public: filepath.Join(scope, "_site"),
Args: []string{}, Args: []string{},
CleanPublic: true, CleanPublic: true,
} }
// Attaches Hugo plugin to this file manager instance. // Attaches Hugo plugin to this file manager instance.
err = m.Attach(jekyll) err = m.Attach(jekyll)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.NoAuth = noAuth m.NoAuth = noAuth
m.SetBaseURL(baseURL) m.SetBaseURL(baseURL)
m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/")) m.SetPrefixURL(strings.TrimSuffix(caddyConf.Addr.Path, "/"))
configs = append(configs, m) configs = append(configs, m)
} }
return configs, nil return configs, nil
} }

View File

@ -1,249 +1,249 @@
package main package main
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/asdine/storm" "github.com/asdine/storm"
lumberjack "gopkg.in/natefinch/lumberjack.v2" lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/hacdias/filemanager" "github.com/hacdias/filemanager"
"github.com/hacdias/filemanager/bolt" "github.com/hacdias/filemanager/bolt"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
"github.com/hacdias/filemanager/staticgen" "github.com/hacdias/filemanager/staticgen"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var ( var (
addr string addr string
config string config string
database string database string
scope string scope string
commands string commands string
logfile string logfile string
staticg string staticg string
locale string locale string
baseurl string baseurl string
prefixurl string prefixurl string
viewMode string viewMode string
recaptchakey string recaptchakey string
recaptchasecret string recaptchasecret string
port int port int
noAuth bool noAuth bool
allowCommands bool allowCommands bool
allowEdit bool allowEdit bool
allowNew bool allowNew bool
allowPublish bool allowPublish bool
showVer bool showVer bool
) )
func init() { func init() {
flag.StringVarP(&config, "config", "c", "", "Configuration file") flag.StringVarP(&config, "config", "c", "", "Configuration file")
flag.IntVarP(&port, "port", "p", 0, "HTTP Port (default is random)") 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(&addr, "address", "a", "", "Address to listen to (default is all of them)")
flag.StringVarP(&database, "database", "d", "./filemanager.db", "Database file") 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(&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(&scope, "scope", "s", ".", "Default scope option for new users")
flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL") flag.StringVarP(&baseurl, "baseurl", "b", "", "Base URL")
flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users") flag.StringVar(&commands, "commands", "git svn hg", "Default commands option for new users")
flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL") flag.StringVar(&prefixurl, "prefixurl", "", "Prefix URL")
flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users") flag.StringVar(&viewMode, "view-mode", "mosaic", "Default view mode for new users")
flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key") flag.StringVar(&recaptchakey, "recaptcha-key", "", "ReCaptcha site key")
flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret") flag.StringVar(&recaptchasecret, "recaptcha-secret", "", "ReCaptcha secret")
flag.BoolVar(&allowCommands, "allow-commands", true, "Default allow commands option for new users") 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(&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(&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(&allowNew, "allow-new", true, "Default allow new option for new users")
flag.BoolVar(&noAuth, "no-auth", false, "Disables authentication") 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(&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.StringVar(&staticg, "staticgen", "", "Static Generator you want to enable")
flag.BoolVarP(&showVer, "version", "v", false, "Show version") flag.BoolVarP(&showVer, "version", "v", false, "Show version")
} }
func setupViper() { func setupViper() {
viper.SetDefault("Address", "") viper.SetDefault("Address", "")
viper.SetDefault("Port", "0") viper.SetDefault("Port", "0")
viper.SetDefault("Database", "./filemanager.db") viper.SetDefault("Database", "./filemanager.db")
viper.SetDefault("Scope", ".") viper.SetDefault("Scope", ".")
viper.SetDefault("Logger", "stdout") viper.SetDefault("Logger", "stdout")
viper.SetDefault("Commands", []string{"git", "svn", "hg"}) viper.SetDefault("Commands", []string{"git", "svn", "hg"})
viper.SetDefault("AllowCommmands", true) viper.SetDefault("AllowCommmands", true)
viper.SetDefault("AllowEdit", true) viper.SetDefault("AllowEdit", true)
viper.SetDefault("AllowNew", true) viper.SetDefault("AllowNew", true)
viper.SetDefault("AllowPublish", true) viper.SetDefault("AllowPublish", true)
viper.SetDefault("StaticGen", "") viper.SetDefault("StaticGen", "")
viper.SetDefault("Locale", "") viper.SetDefault("Locale", "")
viper.SetDefault("NoAuth", false) viper.SetDefault("NoAuth", false)
viper.SetDefault("BaseURL", "") viper.SetDefault("BaseURL", "")
viper.SetDefault("PrefixURL", "") viper.SetDefault("PrefixURL", "")
viper.SetDefault("ViewMode", filemanager.MosaicViewMode) viper.SetDefault("ViewMode", filemanager.MosaicViewMode)
viper.SetDefault("ReCaptchaKey", "") viper.SetDefault("ReCaptchaKey", "")
viper.SetDefault("ReCaptchaSecret", "") viper.SetDefault("ReCaptchaSecret", "")
viper.BindPFlag("Port", flag.Lookup("port")) viper.BindPFlag("Port", flag.Lookup("port"))
viper.BindPFlag("Address", flag.Lookup("address")) viper.BindPFlag("Address", flag.Lookup("address"))
viper.BindPFlag("Database", flag.Lookup("database")) viper.BindPFlag("Database", flag.Lookup("database"))
viper.BindPFlag("Scope", flag.Lookup("scope")) viper.BindPFlag("Scope", flag.Lookup("scope"))
viper.BindPFlag("Logger", flag.Lookup("log")) viper.BindPFlag("Logger", flag.Lookup("log"))
viper.BindPFlag("Commands", flag.Lookup("commands")) viper.BindPFlag("Commands", flag.Lookup("commands"))
viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands")) viper.BindPFlag("AllowCommands", flag.Lookup("allow-commands"))
viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit")) viper.BindPFlag("AllowEdit", flag.Lookup("allow-edit"))
viper.BindPFlag("AlowNew", flag.Lookup("allow-new")) viper.BindPFlag("AlowNew", flag.Lookup("allow-new"))
viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish")) viper.BindPFlag("AllowPublish", flag.Lookup("allow-publish"))
viper.BindPFlag("Locale", flag.Lookup("locale")) viper.BindPFlag("Locale", flag.Lookup("locale"))
viper.BindPFlag("StaticGen", flag.Lookup("staticgen")) viper.BindPFlag("StaticGen", flag.Lookup("staticgen"))
viper.BindPFlag("NoAuth", flag.Lookup("no-auth")) viper.BindPFlag("NoAuth", flag.Lookup("no-auth"))
viper.BindPFlag("BaseURL", flag.Lookup("baseurl")) viper.BindPFlag("BaseURL", flag.Lookup("baseurl"))
viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl")) viper.BindPFlag("PrefixURL", flag.Lookup("prefixurl"))
viper.BindPFlag("ViewMode", flag.Lookup("view-mode")) viper.BindPFlag("ViewMode", flag.Lookup("view-mode"))
viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key")) viper.BindPFlag("ReCaptchaKey", flag.Lookup("recaptcha-key"))
viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret")) viper.BindPFlag("ReCaptchaSecret", flag.Lookup("recaptcha-secret"))
viper.SetConfigName("filemanager") viper.SetConfigName("filemanager")
viper.AddConfigPath(".") viper.AddConfigPath(".")
} }
func printVersion() { func printVersion() {
fmt.Println("filemanager version", filemanager.Version) fmt.Println("filemanager version", filemanager.Version)
os.Exit(0) os.Exit(0)
} }
func main() { func main() {
setupViper() setupViper()
flag.Parse() flag.Parse()
if showVer { if showVer {
printVersion() printVersion()
} }
// Add a configuration file if set. // Add a configuration file if set.
if config != "" { if config != "" {
ext := filepath.Ext(config) ext := filepath.Ext(config)
dir := filepath.Dir(config) dir := filepath.Dir(config)
config = strings.TrimSuffix(config, ext) config = strings.TrimSuffix(config, ext)
if dir != "" { if dir != "" {
viper.AddConfigPath(dir) viper.AddConfigPath(dir)
config = strings.TrimPrefix(config, dir) config = strings.TrimPrefix(config, dir)
} }
viper.SetConfigName(config) viper.SetConfigName(config)
} }
// Read configuration from a file if exists. // Read configuration from a file if exists.
err := viper.ReadInConfig() err := viper.ReadInConfig()
if err != nil { if err != nil {
if _, ok := err.(viper.ConfigParseError); ok { if _, ok := err.(viper.ConfigParseError); ok {
panic(err) panic(err)
} }
} }
// Set up process log before anything bad happens. // Set up process log before anything bad happens.
switch viper.GetString("Logger") { switch viper.GetString("Logger") {
case "stdout": case "stdout":
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
case "stderr": case "stderr":
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
case "": case "":
log.SetOutput(ioutil.Discard) log.SetOutput(ioutil.Discard)
default: default:
log.SetOutput(&lumberjack.Logger{ log.SetOutput(&lumberjack.Logger{
Filename: logfile, Filename: logfile,
MaxSize: 100, MaxSize: 100,
MaxAge: 14, MaxAge: 14,
MaxBackups: 10, MaxBackups: 10,
}) })
} }
// Builds the address and a listener. // Builds the address and a listener.
laddr := viper.GetString("Address") + ":" + viper.GetString("Port") laddr := viper.GetString("Address") + ":" + viper.GetString("Port")
listener, err := net.Listen("tcp", laddr) listener, err := net.Listen("tcp", laddr)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Tell the user the port in which is listening. // Tell the user the port in which is listening.
fmt.Println("Listening on", listener.Addr().String()) fmt.Println("Listening on", listener.Addr().String())
// Starts the server. // Starts the server.
if err := http.Serve(listener, handler()); err != nil { if err := http.Serve(listener, handler()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func handler() http.Handler { func handler() http.Handler {
db, err := storm.Open(viper.GetString("Database")) db, err := storm.Open(viper.GetString("Database"))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
fm := &filemanager.FileManager{ fm := &filemanager.FileManager{
NoAuth: viper.GetBool("NoAuth"), NoAuth: viper.GetBool("NoAuth"),
BaseURL: viper.GetString("BaseURL"), BaseURL: viper.GetString("BaseURL"),
PrefixURL: viper.GetString("PrefixURL"), PrefixURL: viper.GetString("PrefixURL"),
ReCaptchaKey: viper.GetString("ReCaptchaKey"), ReCaptchaKey: viper.GetString("ReCaptchaKey"),
ReCaptchaSecret: viper.GetString("ReCaptchaSecret"), ReCaptchaSecret: viper.GetString("ReCaptchaSecret"),
DefaultUser: &filemanager.User{ DefaultUser: &filemanager.User{
AllowCommands: viper.GetBool("AllowCommands"), AllowCommands: viper.GetBool("AllowCommands"),
AllowEdit: viper.GetBool("AllowEdit"), AllowEdit: viper.GetBool("AllowEdit"),
AllowNew: viper.GetBool("AllowNew"), AllowNew: viper.GetBool("AllowNew"),
AllowPublish: viper.GetBool("AllowPublish"), AllowPublish: viper.GetBool("AllowPublish"),
Commands: viper.GetStringSlice("Commands"), Commands: viper.GetStringSlice("Commands"),
Rules: []*filemanager.Rule{}, Rules: []*filemanager.Rule{},
Locale: viper.GetString("Locale"), Locale: viper.GetString("Locale"),
CSS: "", CSS: "",
Scope: viper.GetString("Scope"), Scope: viper.GetString("Scope"),
FileSystem: fileutils.Dir(viper.GetString("Scope")), FileSystem: fileutils.Dir(viper.GetString("Scope")),
ViewMode: viper.GetString("ViewMode"), ViewMode: viper.GetString("ViewMode"),
}, },
Store: &filemanager.Store{ Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db}, Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db}, Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db}, Share: bolt.ShareStore{DB: db},
}, },
NewFS: func(scope string) filemanager.FileSystem { NewFS: func(scope string) filemanager.FileSystem {
return fileutils.Dir(scope) return fileutils.Dir(scope)
}, },
} }
err = fm.Setup() err = fm.Setup()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
switch viper.GetString("StaticGen") { switch viper.GetString("StaticGen") {
case "hugo": case "hugo":
hugo := &staticgen.Hugo{ hugo := &staticgen.Hugo{
Root: viper.GetString("Scope"), Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "public"), Public: filepath.Join(viper.GetString("Scope"), "public"),
Args: []string{}, Args: []string{},
CleanPublic: true, CleanPublic: true,
} }
if err = fm.Attach(hugo); err != nil { if err = fm.Attach(hugo); err != nil {
log.Fatal(err) log.Fatal(err)
} }
case "jekyll": case "jekyll":
jekyll := &staticgen.Jekyll{ jekyll := &staticgen.Jekyll{
Root: viper.GetString("Scope"), Root: viper.GetString("Scope"),
Public: filepath.Join(viper.GetString("Scope"), "_site"), Public: filepath.Join(viper.GetString("Scope"), "_site"),
Args: []string{"build"}, Args: []string{"build"},
CleanPublic: true, CleanPublic: true,
} }
if err = fm.Attach(jekyll); err != nil { if err = fm.Attach(jekyll); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
return h.Handler(fm) return h.Handler(fm)
} }

146
doc.go
View File

@ -1,73 +1,73 @@
/* /*
Package filemanager provides a web interface to access your files Package filemanager provides a web interface to access your files
wherever you are. To use this package as a middleware for your app, 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. you'll need to import both File Manager and File Manager HTTP packages.
import ( import (
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
h "github.com/hacdias/filemanager/http" h "github.com/hacdias/filemanager/http"
) )
Then, you should create a new FileManager object with your options. In this 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 case, I'm using BoltDB (via Storm package) as a Store. So, you'll also need
to import "github.com/hacdias/filemanager/bolt". to import "github.com/hacdias/filemanager/bolt".
db, _ := storm.Open("bolt.db") db, _ := storm.Open("bolt.db")
m := &fm.FileManager{ m := &fm.FileManager{
NoAuth: false, NoAuth: false,
DefaultUser: &fm.User{ DefaultUser: &fm.User{
AllowCommands: true, AllowCommands: true,
AllowEdit: true, AllowEdit: true,
AllowNew: true, AllowNew: true,
AllowPublish: true, AllowPublish: true,
Commands: []string{"git"}, Commands: []string{"git"},
Rules: []*fm.Rule{}, Rules: []*fm.Rule{},
Locale: "en", Locale: "en",
CSS: "", CSS: "",
Scope: ".", Scope: ".",
FileSystem: fileutils.Dir("."), FileSystem: fileutils.Dir("."),
}, },
Store: &fm.Store{ Store: &fm.Store{
Config: bolt.ConfigStore{DB: db}, Config: bolt.ConfigStore{DB: db},
Users: bolt.UsersStore{DB: db}, Users: bolt.UsersStore{DB: db},
Share: bolt.ShareStore{DB: db}, Share: bolt.ShareStore{DB: db},
}, },
NewFS: func(scope string) fm.FileSystem { NewFS: func(scope string) fm.FileSystem {
return fileutils.Dir(scope) return fileutils.Dir(scope)
}, },
} }
The credentials for the first user are always 'admin' for both the user and 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 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'. 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 Then, you should set the Prefix URL and the Base URL, using the following
functions: functions:
m.SetBaseURL("/") m.SetBaseURL("/")
m.SetPrefixURL("/") m.SetPrefixURL("/")
The Prefix URL is a part of the path that is already stripped from the 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. 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 This is a function that will rarely be used. You can see one example on Caddy
filemanager plugin. filemanager plugin.
The Base URL is the URL path where you want File Manager to be available in. If 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: you want to be available at the root path, you should call:
m.SetBaseURL("/") m.SetBaseURL("/")
But if you want to access it at '/admin', you would call: But if you want to access it at '/admin', you would call:
m.SetBaseURL("/admin") m.SetBaseURL("/admin")
Now, that you already have a File Manager instance created, you just need to 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. 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. 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: One simple implementation for this, at port 80, in the root of the domain, would be:
http.ListenAndServe(":80", h.Handler(m)) http.ListenAndServe(":80", h.Handler(m))
*/ */
package filemanager package filemanager

View File

@ -1,344 +1,344 @@
package http package http
import ( import (
"encoding/json" "encoding/json"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
) )
// Handler returns a function compatible with http.HandleFunc. // Handler returns a function compatible with http.HandleFunc.
func Handler(m *fm.FileManager) http.Handler { func Handler(m *fm.FileManager) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
code, err := serve(&fm.Context{ code, err := serve(&fm.Context{
FileManager: m, FileManager: m,
User: nil, User: nil,
File: nil, File: nil,
}, w, r) }, w, r)
if code >= 400 { if code >= 400 {
w.WriteHeader(code) w.WriteHeader(code)
txt := http.StatusText(code) txt := http.StatusText(code)
log.Printf("%v: %v %v\n", r.URL.Path, code, txt) log.Printf("%v: %v %v\n", r.URL.Path, code, txt)
w.Write([]byte(txt + "\n")) w.Write([]byte(txt + "\n"))
} }
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
}) })
} }
// serve is the main entry point of this HTML application. // serve is the main entry point of this HTML application.
func serve(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { 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 // 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! // returns a 404 fm.Error because we're not supposed to be here!
p := strings.TrimPrefix(r.URL.Path, c.BaseURL) p := strings.TrimPrefix(r.URL.Path, c.BaseURL)
if len(p) >= len(r.URL.Path) && c.BaseURL != "" { if len(p) >= len(r.URL.Path) && c.BaseURL != "" {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
r.URL.Path = p r.URL.Path = p
// Check if this request is made to the service worker. If so, // Check if this request is made to the service worker. If so,
// pass it through a template to add the needed variables. // pass it through a template to add the needed variables.
if r.URL.Path == "/sw.js" { if r.URL.Path == "/sw.js" {
return renderFile(c, w, "sw.js") return renderFile(c, w, "sw.js")
} }
// Checks if this request is made to the static assets folder. If so, and // 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 // if it is a GET request, returns with the asset. Otherwise, returns
// a status not implemented. // a status not implemented.
if matchURL(r.URL.Path, "/static") { if matchURL(r.URL.Path, "/static") {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
return http.StatusNotImplemented, nil return http.StatusNotImplemented, nil
} }
return staticHandler(c, w, r) return staticHandler(c, w, r)
} }
// Checks if this request is made to the API and directs to the // Checks if this request is made to the API and directs to the
// API handler if so. // API handler if so.
if matchURL(r.URL.Path, "/api") { if matchURL(r.URL.Path, "/api") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
return apiHandler(c, w, r) return apiHandler(c, w, r)
} }
// If it is a request to the preview and a static website generator is // If it is a request to the preview and a static website generator is
// active, build the preview. // active, build the preview.
if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil { if strings.HasPrefix(r.URL.Path, "/preview") && c.StaticGen != nil {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview") r.URL.Path = strings.TrimPrefix(r.URL.Path, "/preview")
return c.StaticGen.Preview(c, w, r) return c.StaticGen.Preview(c, w, r)
} }
if strings.HasPrefix(r.URL.Path, "/share/") { if strings.HasPrefix(r.URL.Path, "/share/") {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/") r.URL.Path = strings.TrimPrefix(r.URL.Path, "/share/")
return sharePage(c, w, r) return sharePage(c, w, r)
} }
// Any other request should show the index.html file. // Any other request should show the index.html file.
w.Header().Set("x-frame-options", "SAMEORIGIN") w.Header().Set("x-frame-options", "SAMEORIGIN")
w.Header().Set("x-content-type", "nosniff") w.Header().Set("x-content-type", "nosniff")
w.Header().Set("x-xss-protection", "1; mode=block") w.Header().Set("x-xss-protection", "1; mode=block")
return renderFile(c, w, "index.html") return renderFile(c, w, "index.html")
} }
// staticHandler handles the static assets path. // staticHandler handles the static assets path.
func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path != "/static/manifest.json" { if r.URL.Path != "/static/manifest.json" {
http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r) http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
return renderFile(c, w, "static/manifest.json") return renderFile(c, w, "static/manifest.json")
} }
// apiHandler is the main entry point for the /api endpoint. // apiHandler is the main entry point for the /api endpoint.
func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if r.URL.Path == "/auth/get" { if r.URL.Path == "/auth/get" {
return authHandler(c, w, r) return authHandler(c, w, r)
} }
if r.URL.Path == "/auth/renew" { if r.URL.Path == "/auth/renew" {
return renewAuthHandler(c, w, r) return renewAuthHandler(c, w, r)
} }
valid, _ := validateAuth(c, r) valid, _ := validateAuth(c, r)
if !valid { if !valid {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
c.Router, r.URL.Path = splitURL(r.URL.Path) c.Router, r.URL.Path = splitURL(r.URL.Path)
if !c.User.Allowed(r.URL.Path) { if !c.User.Allowed(r.URL.Path) {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
if c.StaticGen != nil { if c.StaticGen != nil {
// If we are using the 'magic url' for the settings, // If we are using the 'magic url' for the settings,
// we should redirect the request for the acutual path. // we should redirect the request for the acutual path.
if r.URL.Path == "/settings" { if r.URL.Path == "/settings" {
r.URL.Path = c.StaticGen.SettingsPath() r.URL.Path = c.StaticGen.SettingsPath()
} }
// Executes the Static website generator hook. // Executes the Static website generator hook.
code, err := c.StaticGen.Hook(c, w, r) code, err := c.StaticGen.Hook(c, w, r)
if code != 0 || err != nil { if code != 0 || err != nil {
return code, err return code, err
} }
} }
if c.Router == "checksum" || c.Router == "download" { if c.Router == "checksum" || c.Router == "download" {
var err error var err error
c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User) c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil { if err != nil {
return ErrorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
} }
var code int var code int
var err error var err error
switch c.Router { switch c.Router {
case "download": case "download":
code, err = downloadHandler(c, w, r) code, err = downloadHandler(c, w, r)
case "checksum": case "checksum":
code, err = checksumHandler(c, w, r) code, err = checksumHandler(c, w, r)
case "command": case "command":
code, err = command(c, w, r) code, err = command(c, w, r)
case "search": case "search":
code, err = search(c, w, r) code, err = search(c, w, r)
case "resource": case "resource":
code, err = resourceHandler(c, w, r) code, err = resourceHandler(c, w, r)
case "users": case "users":
code, err = usersHandler(c, w, r) code, err = usersHandler(c, w, r)
case "settings": case "settings":
code, err = settingsHandler(c, w, r) code, err = settingsHandler(c, w, r)
case "share": case "share":
code, err = shareHandler(c, w, r) code, err = shareHandler(c, w, r)
default: default:
code = http.StatusNotFound code = http.StatusNotFound
} }
return code, err return code, err
} }
// serveChecksum calculates the hash of a file. Supports MD5, SHA1, SHA256 and SHA512. // 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) { func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
query := r.URL.Query().Get("algo") query := r.URL.Query().Get("algo")
val, err := c.File.Checksum(query) val, err := c.File.Checksum(query)
if err == fm.ErrInvalidOption { if err == fm.ErrInvalidOption {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} else if err != nil { } else if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
w.Write([]byte(val)) w.Write([]byte(val))
return 0, nil return 0, nil
} }
// splitURL splits the path and returns everything that stands // splitURL splits the path and returns everything that stands
// before the first slash and everything that goes after. // before the first slash and everything that goes after.
func splitURL(path string) (string, string) { func splitURL(path string) (string, string) {
if path == "" { if path == "" {
return "", "" return "", ""
} }
path = strings.TrimPrefix(path, "/") path = strings.TrimPrefix(path, "/")
i := strings.Index(path, "/") i := strings.Index(path, "/")
if i == -1 { if i == -1 {
return "", path return "", path
} }
return path[0:i], path[i:] return path[0:i], path[i:]
} }
// renderFile renders a file using a template with some needed variables. // renderFile renders a file using a template with some needed variables.
func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) { func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error) {
tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file))) tpl := template.Must(template.New("file").Parse(c.Assets.MustString(file)))
var contentType string var contentType string
switch filepath.Ext(file) { switch filepath.Ext(file) {
case ".html": case ".html":
contentType = "text/html" contentType = "text/html"
case ".js": case ".js":
contentType = "application/javascript" contentType = "application/javascript"
case ".json": case ".json":
contentType = "application/json" contentType = "application/json"
default: default:
contentType = "text" contentType = "text"
} }
w.Header().Set("Content-Type", contentType+"; charset=utf-8") w.Header().Set("Content-Type", contentType+"; charset=utf-8")
data := map[string]interface{}{ data := map[string]interface{}{
"BaseURL": c.RootURL(), "BaseURL": c.RootURL(),
"NoAuth": c.NoAuth, "NoAuth": c.NoAuth,
"Version": fm.Version, "Version": fm.Version,
"CSS": template.CSS(c.CSS), "CSS": template.CSS(c.CSS),
"ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "", "ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
"ReCaptchaKey": c.ReCaptchaKey, "ReCaptchaKey": c.ReCaptchaKey,
"ReCaptchaSecret": c.ReCaptchaSecret, "ReCaptchaSecret": c.ReCaptchaSecret,
} }
if c.StaticGen != nil { if c.StaticGen != nil {
data["StaticGen"] = c.StaticGen.Name() data["StaticGen"] = c.StaticGen.Name()
} }
err := tpl.Execute(w, data) err := tpl.Execute(w, data)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return 0, nil return 0, nil
} }
// sharePage build the share page. // sharePage build the share page.
func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
s, err := c.Store.Share.Get(r.URL.Path) s, err := c.Store.Share.Get(r.URL.Path)
if err == fm.ErrNotExist { if err == fm.ErrNotExist {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return renderFile(c, w, "static/share/404.html") return renderFile(c, w, "static/share/404.html")
} }
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
if s.Expires && s.ExpireDate.Before(time.Now()) { if s.Expires && s.ExpireDate.Before(time.Now()) {
c.Store.Share.Delete(s.Hash) c.Store.Share.Delete(s.Hash)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return renderFile(c, w, "static/share/404.html") return renderFile(c, w, "static/share/404.html")
} }
r.URL.Path = s.Path r.URL.Path = s.Path
info, err := os.Stat(s.Path) info, err := os.Stat(s.Path)
if err != nil { if err != nil {
c.Store.Share.Delete(s.Hash) c.Store.Share.Delete(s.Hash)
return ErrorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
c.File = &fm.File{ c.File = &fm.File{
Path: s.Path, Path: s.Path,
Name: info.Name(), Name: info.Name(),
ModTime: info.ModTime(), ModTime: info.ModTime(),
Mode: info.Mode(), Mode: info.Mode(),
IsDir: info.IsDir(), IsDir: info.IsDir(),
Size: info.Size(), Size: info.Size(),
} }
dl := r.URL.Query().Get("dl") dl := r.URL.Query().Get("dl")
if dl == "" || dl == "0" { if dl == "" || dl == "0" {
tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html"))) tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html")))
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tpl.Execute(w, map[string]interface{}{ err := tpl.Execute(w, map[string]interface{}{
"BaseURL": c.RootURL(), "BaseURL": c.RootURL(),
"File": c.File, "File": c.File,
}) })
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return 0, nil return 0, nil
} }
return downloadHandler(c, w, r) return downloadHandler(c, w, r)
} }
// renderJSON prints the JSON version of data to the browser. // renderJSON prints the JSON version of data to the browser.
func renderJSON(w http.ResponseWriter, data interface{}) (int, error) { func renderJSON(w http.ResponseWriter, data interface{}) (int, error) {
marsh, err := json.Marshal(data) marsh, err := json.Marshal(data)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write(marsh); err != nil { if _, err := w.Write(marsh); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return 0, nil return 0, nil
} }
// matchURL checks if the first URL matches the second. // matchURL checks if the first URL matches the second.
func matchURL(first, second string) bool { func matchURL(first, second string) bool {
first = strings.ToLower(first) first = strings.ToLower(first)
second = strings.ToLower(second) second = strings.ToLower(second)
return strings.HasPrefix(first, second) return strings.HasPrefix(first, second)
} }
// ErrorToHTTP converts errors to HTTP Status Code. // ErrorToHTTP converts errors to HTTP Status Code.
func ErrorToHTTP(err error, gone bool) int { func ErrorToHTTP(err error, gone bool) int {
switch { switch {
case err == nil: case err == nil:
return http.StatusOK return http.StatusOK
case os.IsPermission(err): case os.IsPermission(err):
return http.StatusForbidden return http.StatusForbidden
case os.IsNotExist(err): case os.IsNotExist(err):
if !gone { if !gone {
return http.StatusNotFound return http.StatusNotFound
} }
return http.StatusGone return http.StatusGone
case os.IsExist(err): case os.IsExist(err):
return http.StatusConflict return http.StatusConflict
default: default:
return http.StatusInternalServerError return http.StatusInternalServerError
} }
} }

View File

@ -1,386 +1,386 @@
package http package http
import ( import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
"github.com/hacdias/fileutils" "github.com/hacdias/fileutils"
) )
// sanitizeURL sanitizes the URL to prevent path transversal // sanitizeURL sanitizes the URL to prevent path transversal
// using fileutils.SlashClean and adds the trailing slash bar. // using fileutils.SlashClean and adds the trailing slash bar.
func sanitizeURL(url string) string { func sanitizeURL(url string) string {
path := fileutils.SlashClean(url) path := fileutils.SlashClean(url)
if strings.HasSuffix(url, "/") && path != "/" { if strings.HasSuffix(url, "/") && path != "/" {
return path + "/" return path + "/"
} }
return path return path
} }
func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
r.URL.Path = sanitizeURL(r.URL.Path) r.URL.Path = sanitizeURL(r.URL.Path)
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
return resourceGetHandler(c, w, r) return resourceGetHandler(c, w, r)
case http.MethodDelete: case http.MethodDelete:
return resourceDeleteHandler(c, w, r) return resourceDeleteHandler(c, w, r)
case http.MethodPut: case http.MethodPut:
// Before save command handler. // Before save command handler.
path := filepath.Join(c.User.Scope, r.URL.Path) path := filepath.Join(c.User.Scope, r.URL.Path)
if err := c.Runner("before_save", path, "", c.User); err != nil { if err := c.Runner("before_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
code, err := resourcePostPutHandler(c, w, r) code, err := resourcePostPutHandler(c, w, r)
if code != http.StatusOK { if code != http.StatusOK {
return code, err return code, err
} }
// After save command handler. // After save command handler.
if err := c.Runner("after_save", path, "", c.User); err != nil { if err := c.Runner("after_save", path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return code, err return code, err
case http.MethodPatch: case http.MethodPatch:
return resourcePatchHandler(c, w, r) return resourcePatchHandler(c, w, r)
case http.MethodPost: case http.MethodPost:
return resourcePostPutHandler(c, w, r) return resourcePostPutHandler(c, w, r)
} }
return http.StatusNotImplemented, nil return http.StatusNotImplemented, nil
} }
func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Gets the information of the directory/file. // Gets the information of the directory/file.
f, err := fm.GetInfo(r.URL, c.FileManager, c.User) f, err := fm.GetInfo(r.URL, c.FileManager, c.User)
if err != nil { if err != nil {
return ErrorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// If it's a dir and the path doesn't end with a trailing slash, // If it's a dir and the path doesn't end with a trailing slash,
// add a trailing slash to the path. // add a trailing slash to the path.
if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") { if f.IsDir && !strings.HasSuffix(r.URL.Path, "/") {
r.URL.Path = r.URL.Path + "/" r.URL.Path = r.URL.Path + "/"
} }
// If it is a dir, go and serve the listing. // If it is a dir, go and serve the listing.
if f.IsDir { if f.IsDir {
c.File = f c.File = f
return listingHandler(c, w, r) return listingHandler(c, w, r)
} }
// Tries to get the file type. // Tries to get the file type.
if err = f.GetFileType(true); err != nil { if err = f.GetFileType(true); err != nil {
return ErrorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
// Serve a preview if the file can't be edited or the // Serve a preview if the file can't be edited or the
// user has no permission to edit this file. Otherwise, // user has no permission to edit this file. Otherwise,
// just serve the editor. // just serve the editor.
if !f.CanBeEdited() || !c.User.AllowEdit { if !f.CanBeEdited() || !c.User.AllowEdit {
f.Kind = "preview" f.Kind = "preview"
return renderJSON(w, f) return renderJSON(w, f)
} }
f.Kind = "editor" f.Kind = "editor"
// Tries to get the editor data. // Tries to get the editor data.
if err = f.GetEditor(); err != nil { if err = f.GetEditor(); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return renderJSON(w, f) return renderJSON(w, f)
} }
func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
f := c.File f := c.File
f.Kind = "listing" f.Kind = "listing"
// Tries to get the listing data. // Tries to get the listing data.
if err := f.GetListing(c.User, r); err != nil { if err := f.GetListing(c.User, r); err != nil {
return ErrorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
listing := f.Listing listing := f.Listing
// Defines the cookie scope. // Defines the cookie scope.
cookieScope := c.RootURL() cookieScope := c.RootURL()
if cookieScope == "" { if cookieScope == "" {
cookieScope = "/" cookieScope = "/"
} }
// Copy the query values into the Listing struct // Copy the query values into the Listing struct
if sort, order, err := handleSortOrder(w, r, cookieScope); err == nil { if sort, order, err := handleSortOrder(w, r, cookieScope); err == nil {
listing.Sort = sort listing.Sort = sort
listing.Order = order listing.Order = order
} else { } else {
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
listing.ApplySort() listing.ApplySort()
return renderJSON(w, f) return renderJSON(w, f)
} }
func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Prevent the removal of the root directory. // Prevent the removal of the root directory.
if r.URL.Path == "/" || !c.User.AllowEdit { if r.URL.Path == "/" || !c.User.AllowEdit {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
// Fire the before trigger. // Fire the before trigger.
if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil { if err := c.Runner("before_delete", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Remove the file or folder. // Remove the file or folder.
err := c.User.FileSystem.RemoveAll(r.URL.Path) err := c.User.FileSystem.RemoveAll(r.URL.Path)
if err != nil { if err != nil {
return ErrorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
// Fire the after trigger. // Fire the after trigger.
if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil { if err := c.Runner("after_delete", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowNew && r.Method == http.MethodPost { if !c.User.AllowNew && r.Method == http.MethodPost {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
if !c.User.AllowEdit && r.Method == http.MethodPut { if !c.User.AllowEdit && r.Method == http.MethodPut {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
// Discard any invalid upload before returning to avoid connection // Discard any invalid upload before returning to avoid connection
// reset error. // reset error.
defer func() { defer func() {
io.Copy(ioutil.Discard, r.Body) io.Copy(ioutil.Discard, r.Body)
}() }()
// Checks if the current request is for a directory and not a file. // Checks if the current request is for a directory and not a file.
if strings.HasSuffix(r.URL.Path, "/") { if strings.HasSuffix(r.URL.Path, "/") {
// If the method is PUT, we return 405 Method not Allowed, because // If the method is PUT, we return 405 Method not Allowed, because
// POST should be used instead. // POST should be used instead.
if r.Method == http.MethodPut { if r.Method == http.MethodPut {
return http.StatusMethodNotAllowed, nil return http.StatusMethodNotAllowed, nil
} }
// Otherwise we try to create the directory. // Otherwise we try to create the directory.
err := c.User.FileSystem.Mkdir(r.URL.Path, 0776) err := c.User.FileSystem.Mkdir(r.URL.Path, 0776)
return ErrorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// If using POST method, we are trying to create a new file so it is not // 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 // desirable to override an already existent file. Thus, we check
// if the file already exists. If so, we just return a 409 Conflict. // if the file already exists. If so, we just return a 409 Conflict.
if r.Method == http.MethodPost && r.Header.Get("Action") != "override" { if r.Method == http.MethodPost && r.Header.Get("Action") != "override" {
if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil { if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil {
return http.StatusConflict, errors.New("There is already a file on that path") return http.StatusConflict, errors.New("There is already a file on that path")
} }
} }
// Fire the before trigger. // Fire the before trigger.
if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil { if err := c.Runner("before_upload", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Create/Open the file. // Create/Open the file.
f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776)
if err != nil { if err != nil {
return ErrorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
defer f.Close() defer f.Close()
// Copies the new content for the file. // Copies the new content for the file.
_, err = io.Copy(f, r.Body) _, err = io.Copy(f, r.Body)
if err != nil { if err != nil {
return ErrorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// Gets the info about the file. // Gets the info about the file.
fi, err := f.Stat() fi, err := f.Stat()
if err != nil { if err != nil {
return ErrorToHTTP(err, false), err return ErrorToHTTP(err, false), err
} }
// Check if this instance has a Static Generator and handles publishing // Check if this instance has a Static Generator and handles publishing
// or scheduling if it's the case. // or scheduling if it's the case.
if c.StaticGen != nil { if c.StaticGen != nil {
code, err := resourcePublishSchedule(c, w, r) code, err := resourcePublishSchedule(c, w, r)
if code != 0 { if code != 0 {
return code, err return code, err
} }
} }
// Writes the ETag Header. // Writes the ETag Header.
etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()) etag := fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
w.Header().Set("ETag", etag) w.Header().Set("ETag", etag)
// Fire the after trigger. // Fire the after trigger.
if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil { if err := c.Runner("after_upload", r.URL.Path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
publish := r.Header.Get("Publish") publish := r.Header.Get("Publish")
schedule := r.Header.Get("Schedule") schedule := r.Header.Get("Schedule")
if publish != "true" && schedule == "" { if publish != "true" && schedule == "" {
return 0, nil return 0, nil
} }
if !c.User.AllowPublish { if !c.User.AllowPublish {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
if publish == "true" { if publish == "true" {
return resourcePublish(c, w, r) return resourcePublish(c, w, r)
} }
t, err := time.Parse("2006-01-02T15:04", schedule) t, err := time.Parse("2006-01-02T15:04", schedule)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() {
_, err := resourcePublish(c, w, r) _, err := resourcePublish(c, w, r)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
}) })
return http.StatusOK, nil return http.StatusOK, nil
} }
func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePublish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
path := filepath.Join(c.User.Scope, r.URL.Path) path := filepath.Join(c.User.Scope, r.URL.Path)
// Before save command handler. // Before save command handler.
if err := c.Runner("before_publish", path, "", c.User); err != nil { if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
code, err := c.StaticGen.Publish(c, w, r) code, err := c.StaticGen.Publish(c, w, r)
if err != nil { if err != nil {
return code, err return code, err
} }
// Executed the before publish command. // Executed the before publish command.
if err := c.Runner("before_publish", path, "", c.User); err != nil { if err := c.Runner("before_publish", path, "", c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return code, nil return code, nil
} }
// resourcePatchHandler is the entry point for resource handler. // resourcePatchHandler is the entry point for resource handler.
func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
if !c.User.AllowEdit { if !c.User.AllowEdit {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
dst := r.Header.Get("Destination") dst := r.Header.Get("Destination")
action := r.Header.Get("Action") action := r.Header.Get("Action")
dst, err := url.QueryUnescape(dst) dst, err := url.QueryUnescape(dst)
if err != nil { if err != nil {
return ErrorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
src := r.URL.Path src := r.URL.Path
if dst == "/" || src == "/" { if dst == "/" || src == "/" {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
if action == "copy" { if action == "copy" {
// Fire the after trigger. // Fire the after trigger.
if err := c.Runner("before_copy", src, dst, c.User); err != nil { if err := c.Runner("before_copy", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Copy the file. // Copy the file.
err = c.User.FileSystem.Copy(src, dst) err = c.User.FileSystem.Copy(src, dst)
// Fire the after trigger. // Fire the after trigger.
if err := c.Runner("after_copy", src, dst, c.User); err != nil { if err := c.Runner("after_copy", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
} else { } else {
// Fire the after trigger. // Fire the after trigger.
if err := c.Runner("before_rename", src, dst, c.User); err != nil { if err := c.Runner("before_rename", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Rename the file. // Rename the file.
err = c.User.FileSystem.Rename(src, dst) err = c.User.FileSystem.Rename(src, dst)
// Fire the after trigger. // Fire the after trigger.
if err := c.Runner("after_rename", src, dst, c.User); err != nil { if err := c.Runner("after_rename", src, dst, c.User); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
} }
return ErrorToHTTP(err, true), err return ErrorToHTTP(err, true), err
} }
// handleSortOrder gets and stores for a Listing the 'sort' and 'order', // 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. // 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) { func handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, err error) {
sort = r.URL.Query().Get("sort") sort = r.URL.Query().Get("sort")
order = r.URL.Query().Get("order") order = r.URL.Query().Get("order")
// If the query 'sort' or 'order' is empty, use defaults or any values // If the query 'sort' or 'order' is empty, use defaults or any values
// previously saved in Cookies. // previously saved in Cookies.
switch sort { switch sort {
case "": case "":
sort = "name" sort = "name"
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil { if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
sort = sortCookie.Value sort = sortCookie.Value
} }
case "name", "size": case "name", "size":
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: "sort", Name: "sort",
Value: sort, Value: sort,
MaxAge: 31536000, MaxAge: 31536000,
Path: scope, Path: scope,
Secure: r.TLS != nil, Secure: r.TLS != nil,
}) })
} }
switch order { switch order {
case "": case "":
order = "asc" order = "asc"
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
order = orderCookie.Value order = orderCookie.Value
} }
case "asc", "desc": case "asc", "desc":
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: "order", Name: "order",
Value: order, Value: order,
MaxAge: 31536000, MaxAge: 31536000,
Path: scope, Path: scope,
Secure: r.TLS != nil, Secure: r.TLS != nil,
}) })
} }
return return
} }

View File

@ -1,339 +1,339 @@
package http package http
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"mime" "mime"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
) )
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
} }
var ( var (
cmdNotImplemented = []byte("Command not implemented.") cmdNotImplemented = []byte("Command not implemented.")
cmdNotAllowed = []byte("Command not allowed.") cmdNotAllowed = []byte("Command not allowed.")
) )
// command handles the requests for VCS related commands: git, svn and mercurial // 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) { func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for fm.Errors. // Upgrades the connection to a websocket and checks for fm.Errors.
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer conn.Close() defer conn.Close()
var ( var (
message []byte message []byte
command []string command []string
) )
// Starts an infinite loop until a valid command is captured. // Starts an infinite loop until a valid command is captured.
for { for {
_, message, err = conn.ReadMessage() _, message, err = conn.ReadMessage()
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
command = strings.Split(string(message), " ") command = strings.Split(string(message), " ")
if len(command) != 0 { if len(command) != 0 {
break break
} }
} }
// Check if the command is allowed // Check if the command is allowed
allowed := false allowed := false
for _, cmd := range c.User.Commands { for _, cmd := range c.User.Commands {
if cmd == command[0] { if cmd == command[0] {
allowed = true allowed = true
} }
} }
if !allowed { if !allowed {
err = conn.WriteMessage(websocket.BinaryMessage, cmdNotAllowed) err = conn.WriteMessage(websocket.BinaryMessage, cmdNotAllowed)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return 0, nil return 0, nil
} }
// Check if the program is talled is installed on the computer. // Check if the program is talled is installed on the computer.
if _, err = exec.LookPath(command[0]); err != nil { if _, err = exec.LookPath(command[0]); err != nil {
err = conn.WriteMessage(websocket.BinaryMessage, cmdNotImplemented) err = conn.WriteMessage(websocket.BinaryMessage, cmdNotImplemented)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return http.StatusNotImplemented, nil return http.StatusNotImplemented, nil
} }
// Gets the path and initializes a buffer. // Gets the path and initializes a buffer.
path := c.User.Scope + "/" + r.URL.Path path := c.User.Scope + "/" + r.URL.Path
path = filepath.Clean(path) path = filepath.Clean(path)
buff := new(bytes.Buffer) buff := new(bytes.Buffer)
// Sets up the command executation. // Sets up the command executation.
cmd := exec.Command(command[0], command[1:]...) cmd := exec.Command(command[0], command[1:]...)
cmd.Dir = path cmd.Dir = path
cmd.Stderr = buff cmd.Stderr = buff
cmd.Stdout = buff cmd.Stdout = buff
// Starts the command and checks for fm.Errors. // Starts the command and checks for fm.Errors.
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Set a 'done' variable to check whetever the command has already finished // 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 // running or not. This verification is done using a goroutine that uses the
// method .Wait() from the command. // method .Wait() from the command.
done := false done := false
go func() { go func() {
err = cmd.Wait() err = cmd.Wait()
done = true done = true
}() }()
// Function to print the current information on the buffer to the connection. // Function to print the current information on the buffer to the connection.
print := func() error { print := func() error {
by := buff.Bytes() by := buff.Bytes()
if len(by) > 0 { if len(by) > 0 {
err = conn.WriteMessage(websocket.TextMessage, by) err = conn.WriteMessage(websocket.TextMessage, by)
if err != nil { if err != nil {
return err return err
} }
} }
return nil return nil
} }
// While the command hasn't finished running, continue sending the output // While the command hasn't finished running, continue sending the output
// to the client in intervals of 100 milliseconds. // to the client in intervals of 100 milliseconds.
for !done { for !done {
if err = print(); err != nil { if err = print(); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
// After the command is done executing, send the output one more time to the // After the command is done executing, send the output one more time to the
// browser to make sure it gets the latest information. // browser to make sure it gets the latest information.
if err = print(); err != nil { if err = print(); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return 0, nil return 0, nil
} }
var ( var (
typeRegexp = regexp.MustCompile(`type:(\w+)`) typeRegexp = regexp.MustCompile(`type:(\w+)`)
) )
type condition func(path string) bool type condition func(path string) bool
type searchOptions struct { type searchOptions struct {
CaseInsensitive bool CaseInsensitive bool
Conditions []condition Conditions []condition
Terms []string Terms []string
} }
func extensionCondition(extension string) condition { func extensionCondition(extension string) condition {
return func(path string) bool { return func(path string) bool {
return filepath.Ext(path) == "."+extension return filepath.Ext(path) == "."+extension
} }
} }
func imageCondition(path string) bool { func imageCondition(path string) bool {
extension := filepath.Ext(path) extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension) mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "image") return strings.HasPrefix(mimetype, "image")
} }
func audioCondition(path string) bool { func audioCondition(path string) bool {
extension := filepath.Ext(path) extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension) mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "audio") return strings.HasPrefix(mimetype, "audio")
} }
func videoCondition(path string) bool { func videoCondition(path string) bool {
extension := filepath.Ext(path) extension := filepath.Ext(path)
mimetype := mime.TypeByExtension(extension) mimetype := mime.TypeByExtension(extension)
return strings.HasPrefix(mimetype, "video") return strings.HasPrefix(mimetype, "video")
} }
func parseSearch(value string) *searchOptions { func parseSearch(value string) *searchOptions {
opts := &searchOptions{ opts := &searchOptions{
CaseInsensitive: strings.Contains(value, "case:insensitive"), CaseInsensitive: strings.Contains(value, "case:insensitive"),
Conditions: []condition{}, Conditions: []condition{},
Terms: []string{}, Terms: []string{},
} }
// removes the options from the value // removes the options from the value
value = strings.Replace(value, "case:insensitive", "", -1) value = strings.Replace(value, "case:insensitive", "", -1)
value = strings.Replace(value, "case:sensitive", "", -1) value = strings.Replace(value, "case:sensitive", "", -1)
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
types := typeRegexp.FindAllStringSubmatch(value, -1) types := typeRegexp.FindAllStringSubmatch(value, -1)
for _, t := range types { for _, t := range types {
if len(t) == 1 { if len(t) == 1 {
continue continue
} }
switch t[1] { switch t[1] {
case "image": case "image":
opts.Conditions = append(opts.Conditions, imageCondition) opts.Conditions = append(opts.Conditions, imageCondition)
case "audio", "music": case "audio", "music":
opts.Conditions = append(opts.Conditions, audioCondition) opts.Conditions = append(opts.Conditions, audioCondition)
case "video": case "video":
opts.Conditions = append(opts.Conditions, videoCondition) opts.Conditions = append(opts.Conditions, videoCondition)
default: default:
opts.Conditions = append(opts.Conditions, extensionCondition(t[1])) opts.Conditions = append(opts.Conditions, extensionCondition(t[1]))
} }
} }
if len(types) > 0 { if len(types) > 0 {
// Remove the fields from the search value. // Remove the fields from the search value.
value = typeRegexp.ReplaceAllString(value, "") value = typeRegexp.ReplaceAllString(value, "")
} }
// If it's canse insensitive, put everything in lowercase. // If it's canse insensitive, put everything in lowercase.
if opts.CaseInsensitive { if opts.CaseInsensitive {
value = strings.ToLower(value) value = strings.ToLower(value)
} }
// Remove the spaces from the search value. // Remove the spaces from the search value.
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
return opts return opts
} }
// if the value starts with " and finishes what that character, we will // if the value starts with " and finishes what that character, we will
// only search for that term // only search for that term
if value[0] == '"' && value[len(value)-1] == '"' { if value[0] == '"' && value[len(value)-1] == '"' {
unique := strings.TrimPrefix(value, "\"") unique := strings.TrimPrefix(value, "\"")
unique = strings.TrimSuffix(unique, "\"") unique = strings.TrimSuffix(unique, "\"")
opts.Terms = []string{unique} opts.Terms = []string{unique}
return opts return opts
} }
opts.Terms = strings.Split(value, " ") opts.Terms = strings.Split(value, " ")
return opts return opts
} }
// search searches for a file or directory. // search searches for a file or directory.
func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Upgrades the connection to a websocket and checks for fm.Errors. // Upgrades the connection to a websocket and checks for fm.Errors.
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer conn.Close() defer conn.Close()
var ( var (
value string value string
search *searchOptions search *searchOptions
message []byte message []byte
) )
// Starts an infinite loop until a valid command is captured. // Starts an infinite loop until a valid command is captured.
for { for {
_, message, err = conn.ReadMessage() _, message, err = conn.ReadMessage()
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
if len(message) != 0 { if len(message) != 0 {
value = string(message) value = string(message)
break break
} }
} }
search = parseSearch(value) search = parseSearch(value)
scope := strings.TrimPrefix(r.URL.Path, "/") scope := strings.TrimPrefix(r.URL.Path, "/")
scope = "/" + scope scope = "/" + scope
scope = c.User.Scope + scope scope = c.User.Scope + scope
scope = strings.Replace(scope, "\\", "/", -1) scope = strings.Replace(scope, "\\", "/", -1)
scope = filepath.Clean(scope) scope = filepath.Clean(scope)
err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error { err = filepath.Walk(scope, func(path string, f os.FileInfo, err error) error {
if search.CaseInsensitive { if search.CaseInsensitive {
path = strings.ToLower(path) path = strings.ToLower(path)
} }
path = strings.TrimPrefix(path, scope) path = strings.TrimPrefix(path, scope)
path = strings.TrimPrefix(path, "/") path = strings.TrimPrefix(path, "/")
path = strings.Replace(path, "\\", "/", -1) path = strings.Replace(path, "\\", "/", -1)
// Only execute if there are conditions to meet. // Only execute if there are conditions to meet.
if len(search.Conditions) > 0 { if len(search.Conditions) > 0 {
match := false match := false
for _, t := range search.Conditions { for _, t := range search.Conditions {
if t(path) { if t(path) {
match = true match = true
break break
} }
} }
// If doesn't meet the condition, go to the next. // If doesn't meet the condition, go to the next.
if !match { if !match {
return nil return nil
} }
} }
if len(search.Terms) > 0 { if len(search.Terms) > 0 {
is := false is := false
// Checks if matches the terms and if it is allowed. // Checks if matches the terms and if it is allowed.
for _, term := range search.Terms { for _, term := range search.Terms {
if is { if is {
break break
} }
if strings.Contains(path, term) { if strings.Contains(path, term) {
if !c.User.Allowed(path) { if !c.User.Allowed(path) {
return nil return nil
} }
is = true is = true
} }
} }
if !is { if !is {
return nil return nil
} }
} }
response, _ := json.Marshal(map[string]interface{}{ response, _ := json.Marshal(map[string]interface{}{
"dir": f.IsDir(), "dir": f.IsDir(),
"path": path, "path": path,
}) })
return conn.WriteMessage(websocket.TextMessage, response) return conn.WriteMessage(websocket.TextMessage, response)
}) })
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
return 0, nil return 0, nil
} }

View File

@ -1 +0,0 @@
882c59547968e4fb9a65aa8d1c838409f198e51b

View File

@ -1,194 +1,194 @@
package staticgen package staticgen
import ( import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
"github.com/hacdias/varutils" "github.com/hacdias/varutils"
) )
var ( var (
errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action") errUnsupportedFileType = errors.New("The type of the provided file isn't supported for this action")
) )
// Hugo is the Hugo static website generator. // Hugo is the Hugo static website generator.
type Hugo struct { type Hugo struct {
// Website root // Website root
Root string `name:"Website Root"` Root string `name:"Website Root"`
// Public folder // Public folder
Public string `name:"Public Directory"` Public string `name:"Public Directory"`
// Hugo executable path // Hugo executable path
Exe string `name:"Hugo Executable"` Exe string `name:"Hugo Executable"`
// Hugo arguments // Hugo arguments
Args []string `name:"Hugo Arguments"` Args []string `name:"Hugo Arguments"`
// Indicates if we should clean public before a new publish. // Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"` CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview // previewPath is the temporary path for a preview
previewPath string previewPath string
} }
// SettingsPath retrieves the correct settings path. // SettingsPath retrieves the correct settings path.
func (h Hugo) SettingsPath() string { func (h Hugo) SettingsPath() string {
var frontmatter string var frontmatter string
var err error var err error
if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil { if _, err = os.Stat(filepath.Join(h.Root, "config.yaml")); err == nil {
frontmatter = "yaml" frontmatter = "yaml"
} }
if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil { if _, err = os.Stat(filepath.Join(h.Root, "config.json")); err == nil {
frontmatter = "json" frontmatter = "json"
} }
if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil { if _, err = os.Stat(filepath.Join(h.Root, "config.toml")); err == nil {
frontmatter = "toml" frontmatter = "toml"
} }
if frontmatter == "" { if frontmatter == "" {
return "/settings" return "/settings"
} }
return "/config." + frontmatter return "/config." + frontmatter
} }
// Name is the plugin's name. // Name is the plugin's name.
func (h Hugo) Name() string { func (h Hugo) Name() string {
return "hugo" return "hugo"
} }
// Hook is the pre-api handler. // Hook is the pre-api handler.
func (h Hugo) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { 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 // If we are not using HTTP Post, we shall return Method Not Allowed
// since we are only working with this method. // since we are only working with this method.
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
return 0, nil return 0, nil
} }
if c.Router != "resource" { if c.Router != "resource" {
return 0, nil return 0, nil
} }
// We only care about creating new files from archetypes here. So... // We only care about creating new files from archetypes here. So...
if r.Header.Get("Archetype") == "" { if r.Header.Get("Archetype") == "" {
return 0, nil return 0, nil
} }
if !c.User.AllowNew { if !c.User.AllowNew {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} }
filename := filepath.Join(c.User.Scope, r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
archetype := r.Header.Get("archetype") archetype := r.Header.Get("archetype")
ext := filepath.Ext(filename) ext := filepath.Ext(filename)
// If the request isn't for a markdown file, we can't // If the request isn't for a markdown file, we can't
// handle it. // handle it.
if ext != ".markdown" && ext != ".md" { if ext != ".markdown" && ext != ".md" {
return http.StatusBadRequest, errUnsupportedFileType return http.StatusBadRequest, errUnsupportedFileType
} }
// Tries to create a new file based on this archetype. // Tries to create a new file based on this archetype.
args := []string{"new", filename, "--kind", archetype} args := []string{"new", filename, "--kind", archetype}
if err := runCommand(h.Exe, args, h.Root); err != nil { if err := runCommand(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Writes the location of the new file to the Header. // Writes the location of the new file to the Header.
w.Header().Set("Location", "/files/content/"+filename) w.Header().Set("Location", "/files/content/"+filename)
return http.StatusCreated, nil return http.StatusCreated, nil
} }
// Publish publishes a post. // Publish publishes a post.
func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func (h Hugo) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(c.User.Scope, r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file. // We only run undraft command if it is a file.
if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") { if strings.HasSuffix(filename, ".md") && strings.HasSuffix(filename, ".markdown") {
if err := h.undraft(filename); err != nil { if err := h.undraft(filename); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
} }
// Regenerates the file // Regenerates the file
h.run(false) h.run(false)
return 0, nil return 0, nil
} }
// Preview handles the preview path. // Preview handles the preview path.
func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func (h *Hugo) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none. // Get a new temporary path if there is none.
if h.previewPath == "" { if h.previewPath == "" {
path, err := ioutil.TempDir("", "") path, err := ioutil.TempDir("", "")
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
h.previewPath = path h.previewPath = path
} }
// Build the arguments to execute Hugo: change the base URL, // Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination. // build the drafts and update the destination.
args := h.Args args := h.Args
args = append(args, "--baseURL", c.RootURL()+"/preview/") args = append(args, "--baseURL", c.RootURL()+"/preview/")
args = append(args, "--buildDrafts") args = append(args, "--buildDrafts")
args = append(args, "--destination", h.previewPath) args = append(args, "--destination", h.previewPath)
// Builds the preview. // Builds the preview.
if err := runCommand(h.Exe, args, h.Root); err != nil { if err := runCommand(h.Exe, args, h.Root); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Serves the temporary path with the preview. // Serves the temporary path with the preview.
http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r) http.FileServer(http.Dir(h.previewPath)).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
func (h Hugo) run(force bool) { func (h Hugo) run(force bool) {
// If the CleanPublic option is enabled, clean it. // If the CleanPublic option is enabled, clean it.
if h.CleanPublic { if h.CleanPublic {
os.RemoveAll(h.Public) os.RemoveAll(h.Public)
} }
// Prevent running if watching is enabled // Prevent running if watching is enabled
if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force { if b, pos := varutils.StringInSlice("--watch", h.Args); b && !force {
if len(h.Args) > pos && h.Args[pos+1] != "false" { if len(h.Args) > pos && h.Args[pos+1] != "false" {
return return
} }
if len(h.Args) == pos+1 { if len(h.Args) == pos+1 {
return return
} }
} }
if err := runCommand(h.Exe, h.Args, h.Root); err != nil { if err := runCommand(h.Exe, h.Args, h.Root); err != nil {
log.Println(err) log.Println(err)
} }
} }
func (h Hugo) undraft(file string) error { func (h Hugo) undraft(file string) error {
args := []string{"undraft", file} args := []string{"undraft", file}
if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") { if err := runCommand(h.Exe, args, h.Root); err != nil && !strings.Contains(err.Error(), "not a Draft") {
return err return err
} }
return nil return nil
} }
// Setup sets up the plugin. // Setup sets up the plugin.
func (h *Hugo) Setup() error { func (h *Hugo) Setup() error {
var err error var err error
if h.Exe, err = exec.LookPath("hugo"); err != nil { if h.Exe, err = exec.LookPath("hugo"); err != nil {
return err return err
} }
return nil return nil
} }

View File

@ -1,125 +1,125 @@
package staticgen package staticgen
import ( import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
fm "github.com/hacdias/filemanager" fm "github.com/hacdias/filemanager"
) )
// Jekyll is the Jekyll static website generator. // Jekyll is the Jekyll static website generator.
type Jekyll struct { type Jekyll struct {
// Website root // Website root
Root string `name:"Website Root"` Root string `name:"Website Root"`
// Public folder // Public folder
Public string `name:"Public Directory"` Public string `name:"Public Directory"`
// Jekyll executable path // Jekyll executable path
Exe string `name:"Executable"` Exe string `name:"Executable"`
// Jekyll arguments // Jekyll arguments
Args []string `name:"Arguments"` Args []string `name:"Arguments"`
// Indicates if we should clean public before a new publish. // Indicates if we should clean public before a new publish.
CleanPublic bool `name:"Clean Public"` CleanPublic bool `name:"Clean Public"`
// previewPath is the temporary path for a preview // previewPath is the temporary path for a preview
previewPath string previewPath string
} }
// Name is the plugin's name. // Name is the plugin's name.
func (j Jekyll) Name() string { func (j Jekyll) Name() string {
return "jekyll" return "jekyll"
} }
// SettingsPath retrieves the correct settings path. // SettingsPath retrieves the correct settings path.
func (j Jekyll) SettingsPath() string { func (j Jekyll) SettingsPath() string {
return "/_config.yml" return "/_config.yml"
} }
// Hook is the pre-api handler. // Hook is the pre-api handler.
func (j Jekyll) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func (j Jekyll) Hook(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil return 0, nil
} }
// Publish publishes a post. // Publish publishes a post.
func (j Jekyll) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func (j Jekyll) Publish(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
filename := filepath.Join(c.User.Scope, r.URL.Path) filename := filepath.Join(c.User.Scope, r.URL.Path)
// We only run undraft command if it is a file. // We only run undraft command if it is a file.
if err := j.undraft(filename); err != nil { if err := j.undraft(filename); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Regenerates the file // Regenerates the file
j.run() j.run()
return 0, nil return 0, nil
} }
// Preview handles the preview path. // Preview handles the preview path.
func (j *Jekyll) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { func (j *Jekyll) Preview(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// Get a new temporary path if there is none. // Get a new temporary path if there is none.
if j.previewPath == "" { if j.previewPath == "" {
path, err := ioutil.TempDir("", "") path, err := ioutil.TempDir("", "")
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
j.previewPath = path j.previewPath = path
} }
// Build the arguments to execute Hugo: change the base URL, // Build the arguments to execute Hugo: change the base URL,
// build the drafts and update the destination. // build the drafts and update the destination.
args := j.Args args := j.Args
args = append(args, "--baseurl", c.RootURL()+"/preview/") args = append(args, "--baseurl", c.RootURL()+"/preview/")
args = append(args, "--drafts") args = append(args, "--drafts")
args = append(args, "--destination", j.previewPath) args = append(args, "--destination", j.previewPath)
// Builds the preview. // Builds the preview.
if err := runCommand(j.Exe, args, j.Root); err != nil { if err := runCommand(j.Exe, args, j.Root); err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
// Serves the temporary path with the preview. // Serves the temporary path with the preview.
http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r) http.FileServer(http.Dir(j.previewPath)).ServeHTTP(w, r)
return 0, nil return 0, nil
} }
func (j Jekyll) run() { func (j Jekyll) run() {
// If the CleanPublic option is enabled, clean it. // If the CleanPublic option is enabled, clean it.
if j.CleanPublic { if j.CleanPublic {
os.RemoveAll(j.Public) os.RemoveAll(j.Public)
} }
if err := runCommand(j.Exe, j.Args, j.Root); err != nil { if err := runCommand(j.Exe, j.Args, j.Root); err != nil {
log.Println(err) log.Println(err)
} }
} }
func (j Jekyll) undraft(file string) error { func (j Jekyll) undraft(file string) error {
if !strings.Contains(file, "_drafts") { if !strings.Contains(file, "_drafts") {
return nil return nil
} }
return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1)) return os.Rename(file, strings.Replace(file, "_drafts", "_posts", 1))
} }
// Setup sets up the plugin. // Setup sets up the plugin.
func (j *Jekyll) Setup() error { func (j *Jekyll) Setup() error {
var err error var err error
if j.Exe, err = exec.LookPath("jekyll"); err != nil { if j.Exe, err = exec.LookPath("jekyll"); err != nil {
return err return err
} }
if len(j.Args) == 0 { if len(j.Args) == 0 {
j.Args = []string{"build"} j.Args = []string{"build"}
} }
if j.Args[0] != "build" { if j.Args[0] != "build" {
j.Args = append([]string{"build"}, j.Args...) j.Args = append([]string{"build"}, j.Args...)
} }
return nil return nil
} }

View File

@ -1,19 +1,19 @@
package staticgen package staticgen
import ( import (
"errors" "errors"
"os/exec" "os/exec"
) )
// runCommand executes an external command // runCommand executes an external command
func runCommand(command string, args []string, path string) error { func runCommand(command string, args []string, path string) error {
cmd := exec.Command(command, args...) cmd := exec.Command(command, args...)
cmd.Dir = path cmd.Dir = path
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return errors.New(string(out)) return errors.New(string(out))
} }
return nil return nil
} }