Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d58fd6434f | ||
|
|
0f9733c890 | ||
|
|
14643d3cf4 | ||
|
|
d49f837b2e | ||
|
|
cb19d235c6 | ||
|
|
d41d1d8a34 | ||
|
|
b6d9c7816c | ||
|
|
7345d065b8 | ||
|
|
3211e8607d | ||
|
|
be3e0fa2c2 | ||
|
|
41d24fe134 | ||
|
|
e9f7141f65 | ||
|
|
d7e60e1e23 | ||
|
|
1ed6410380 | ||
|
|
42783f8886 | ||
|
|
80054e8aa9 | ||
|
|
7be4f10441 | ||
|
|
a0a8b57992 | ||
|
|
e8ee0862ef | ||
|
|
1b006b83ad | ||
|
|
71527eab2b | ||
|
|
1576c57241 | ||
|
|
187d602c91 | ||
|
|
e88cd2d880 | ||
|
|
4baea3e63f | ||
|
|
eeed7ad847 | ||
|
|
9f27939471 | ||
|
|
5e61291703 | ||
|
|
59174b40ff | ||
|
|
359c768041 | ||
|
|
a64861ce63 | ||
|
|
115d50f793 | ||
|
|
e4b5856338 | ||
|
|
a8cbfb1ec0 | ||
|
|
c4bb20fd12 | ||
|
|
5eba524bfc | ||
|
|
0ba46039a2 | ||
|
|
340df9ce16 | ||
|
|
eaa15d2905 | ||
|
|
29413c0e52 | ||
|
|
347e79610e | ||
|
|
c084792d32 | ||
|
|
d77e636e53 | ||
|
|
4729252c63 | ||
|
|
6cbb82a927 | ||
|
|
bba9030f14 | ||
|
|
7cf6dbbe38 | ||
|
|
00993e7fe1 | ||
|
|
0fb92efbf3 | ||
|
|
2856da6888 | ||
|
|
c01a6dc9e9 | ||
|
|
845c96cd48 | ||
|
|
094d18be46 | ||
|
|
c6937fd184 | ||
|
|
430744a324 | ||
|
|
98c1589a1a | ||
|
|
d81bb8c893 | ||
|
|
21b774143b | ||
|
|
3925adc3f2 |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
test-results/
|
||||
tmp/
|
||||
routes/
|
||||
/none
|
||||
/src
|
||||
pkg
|
||||
bin2
|
||||
@@ -14,5 +18,11 @@ app/tmp/main.go
|
||||
.settings
|
||||
.project
|
||||
public/config.codekit
|
||||
files
|
||||
files/
|
||||
/node_modules
|
||||
.idea
|
||||
*.iml
|
||||
target/
|
||||
package/leanote.tar.gz
|
||||
package/leanote/
|
||||
leanote.log
|
||||
17
.project
17
.project
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>leanote-public</name>
|
||||
<comment>leanote, your own cloud note!</comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.googlecode.goclipse.goBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>goclipse.goNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
26
.travis.yml
26
.travis.yml
@@ -1,29 +1,41 @@
|
||||
language: go
|
||||
go: 1.7
|
||||
go: 1.15
|
||||
|
||||
services:
|
||||
- mongodb # 2.4.12
|
||||
|
||||
install:
|
||||
- go version
|
||||
- export PATH=$PATH:$HOME/gopath/bin
|
||||
- go get -v github.com/leanote/leanote/app
|
||||
# - go get -v github.com/leanote/leanote/app
|
||||
- go get -u github.com/revel/cmd/revel
|
||||
- ls $GOPATH/src/github.com/revel/
|
||||
# - ls $GOPATH/src/github.com/revel/
|
||||
# - go get github.com/revel/moudle/revel
|
||||
# - go install github.com/revel/cmd/revel
|
||||
- revel version
|
||||
- pwd
|
||||
- ls
|
||||
|
||||
script:
|
||||
- wget https://github.com/leanote/leanote/archive/refs/heads/master.zip
|
||||
- unzip master.zip
|
||||
- mv leanote-master leanote
|
||||
- cd leanote
|
||||
|
||||
- mongo --version
|
||||
- mongorestore -h localhost -d leanote --directoryperdb ./mongodb_backup/leanote_install_data/
|
||||
- mongorestore -h localhost -d leanote --dir ./mongodb_backup/leanote_install_data/
|
||||
|
||||
- cd ./sh
|
||||
# - cd $GOPATH/src/github.com/leanote/leanote/sh
|
||||
- sh run.sh &
|
||||
|
||||
# gen tmp/main.go, routes/routes.go
|
||||
- go run app/cmd/main.go
|
||||
#- go run app/cmd/main.go
|
||||
# build
|
||||
- go build -o leanote github.com/leanote/leanote/app/tmp
|
||||
#- go build -o leanote github.com/leanote/leanote/app/tmp
|
||||
# run with port 9000
|
||||
- ./leanote -importPath=github.com/leanote/leanote -runMode=dev -port=9000 &
|
||||
#- ./leanote -importPath=github.com/leanote/leanote -runMode=dev -port=9000 &
|
||||
|
||||
- sleep 10s;
|
||||
# test
|
||||
- curl http://localhost:9000
|
||||
|
||||
359
LICENSE
359
LICENSE
@@ -1,359 +0,0 @@
|
||||
LEANOTE - NOT JUST A NOTEPAD!
|
||||
|
||||
Copyright 2014-2015 by the contributors
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
Leanote is licensed under the GPL v2.
|
||||
|
||||
life(life@leanote.com, lifephp@gmail.com)
|
||||
|
||||
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
13
README.md
13
README.md
@@ -31,7 +31,7 @@ We have been using the popular note-taking software/service `Evernote` as our kn
|
||||
|
||||
* A more powerful editor: `Evernote`'s editor lacks the functionalities of **document navigation**, **syntax based code rendering** (as a programmer, syntax highlighted code rendering is a necessity), **image resizing** and so forth.
|
||||
* Everybody loves *markdown*, however `Evernote` simply wouldn't add it despite of years' of requests from users. So we will do the favor and bring a *markdown* enabled editor to you, guess what, it is also rendered in real-time!
|
||||
* If you a developer and miss the `Vim` or `Emacs` ways of writing, we offer you the choice of `Vim` and `Emacs` editting modes. Equipped with *markdown* syntax for text formatting, you will never need to touch your mouse while writing.
|
||||
* If you a developer and miss the `Vim` or `Emacs` ways of writing, we offer you the choice of `Vim` and `Emacs` editing modes. Equipped with *markdown* syntax for text formatting, you will never need to touch your mouse while writing.
|
||||
* We love managing knowledge and thoughts as much as sharing them, so everybody has their own note account (`Evernote`, `Onenote`, `Google doc`, `Wiz note` etc.) and social media account (`Facebook`, `Wordpress`, blogs, etc.). But why can’t those two be one? Leanote makes this first step to bridge the private note-taking and public knowledge sharing seamlessly.
|
||||
* A complete and all-platform (sorry Windows phone) covering software suite: that includes Leanote Web & Server (this repository), [Desktop app](https://github.com/leanote/desktop-app), [iOS](https://github.com/leanote/leanote-ios), [Android](https://github.com/leanote/leanote-android). And they are all open source!
|
||||
* ......
|
||||
@@ -50,7 +50,7 @@ More information about how to install Leanote please see:
|
||||
* [Windows](https://github.com/leanote/leanote/wiki/leanote-source-installation-on-Windows-(En))
|
||||
* [Mac and Linux](https://github.com/leanote/leanote/wiki/leanote-binary-installation-on-Mac-and-Linux-(En))
|
||||
* Leanote source installation tutorial:
|
||||
* [Windows](https://github.com/leanote/leanote/wiki/leanote-source-installation-on-Windows-(En))
|
||||
<!-- * [Windows](https://github.com/leanote/leanote/wiki/leanote-source-installation-on-Windows-(En)) -->
|
||||
* [Mac and Linux](https://github.com/leanote/leanote/wiki/Leanote-source-installation-on-Mac-and-Linux-(En))
|
||||
|
||||
## 4. Documentation
|
||||
@@ -66,7 +66,7 @@ If you are a developer yourself and feel like to build on top of Leanote, please
|
||||
## 6. Contributions
|
||||
|
||||
Like or dislike Leanote, please leave your comments and suggestions to help us improve it.
|
||||
If you encounter any issue, we suggest you first do a search in the issues section to see whether a solution already exists, or open up a new one otherwise.
|
||||
If you encounter any issue, we suggest you first search the issues section to see whether a solution already exists, or open up a new one otherwise.
|
||||
|
||||
We’d like to acknowledge the contributions made by our [developers and contributors](https://github.com/leanote/leanote/graphs/contributors) to
|
||||
this project. Leanote won’t exist without your hard work. Your help is much appreciated.
|
||||
@@ -81,12 +81,12 @@ If you find any problems or have any good ideas, feature requests, please submit
|
||||
## 8. Donation
|
||||
|
||||
If you like our product, consider supporting us via [donate us](http://leanote.org/#donate).
|
||||
We acknowledge the donations make by all the [donators](http://leanote.leanote.com/post/leanote-donation-list).
|
||||
We acknowledge the donations made by all the [donators](http://leanote.leanote.com/post/leanote-donation-list).
|
||||
|
||||
## 9. Related projects
|
||||
|
||||
* [Leanote Desktop App](https://github.com/leanote/desktop-app), [Download](http://app.leanote.com)
|
||||
* [Leanote iOS](https://github.com/leanote/leanote-ios), [Download From App Store](https://itunes.apple.com/en/app/leanote/id1022302858?mt=8)
|
||||
* [Leanote iOS](https://github.com/leanote/leanote-ios), [Download From App Store](https://itunes.apple.com/app/leanote/id1022302858)
|
||||
* [Leanote Android](https://github.com/leanote/leanote-android), development phase
|
||||
|
||||
You are welcome to join us.
|
||||
@@ -145,7 +145,7 @@ Leanote云笔记产品包括: Leanote Web & Server(即本仓库), 桌面客户
|
||||
* [Windows](https://github.com/leanote/leanote/wiki/Leanote%E4%BA%8C%E8%BF%9B%E5%88%B6%E7%89%88%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B---Windows)
|
||||
* [Mac, Linux](https://github.com/leanote/leanote/wiki/leanote%E4%BA%8C%E8%BF%9B%E5%88%B6%E7%89%88%E8%AF%A6%E7%BB%86%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B)
|
||||
* Leanote源码详细安装教程:
|
||||
* [Windows](https://github.com/leanote/leanote/wiki/Leanote-%E6%BA%90%E7%A0%81%E7%89%88%E8%AF%A6%E7%BB%86%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B----Windows)
|
||||
<!-- * [Windows](https://github.com/leanote/leanote/wiki/Leanote-%E6%BA%90%E7%A0%81%E7%89%88%E8%AF%A6%E7%BB%86%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B----Windows) -->
|
||||
* [Mac, Linux](https://github.com/leanote/leanote/wiki/leanote%E5%BC%80%E5%8F%91%E7%89%88%E8%AF%A6%E7%BB%86%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B)
|
||||
|
||||
## 4. 相关文档
|
||||
@@ -186,4 +186,3 @@ Leanote还有很多问题, 如果你喜欢它, 欢迎加入我们一起完善Lea
|
||||
* [Leanote 社区](http://bbs.leanote.com)
|
||||
* [QQ群](http://leanote.leanote.com/post/Leanote-groups)
|
||||
* [Leanote Google Group](https://groups.google.com/forum/#!forum/leanote)
|
||||
|
||||
|
||||
9
app/cmd/README.md
Normal file
9
app/cmd/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
全部代码来自https://github.com/revel/cmd
|
||||
|
||||
因为要改parse2, 所以改只要一点点代码
|
||||
harness/
|
||||
build.go 只要gensource, 其它的先return
|
||||
main.go 改动很小
|
||||
build.go 改动很小
|
||||
parser2/
|
||||
source_processors.go 改了 fsWalk 过滤掉 public, files, build 等文件夹
|
||||
270
app/cmd/build.go
Normal file
270
app/cmd/build.go
Normal file
@@ -0,0 +1,270 @@
|
||||
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
||||
// Revel Framework source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
"github.com/leanote/leanote/app/cmd/harness" // 只改了这个
|
||||
"github.com/revel/cmd/model"
|
||||
"github.com/revel/cmd/utils"
|
||||
)
|
||||
|
||||
var cmdBuild = &Command{
|
||||
UsageLine: "revel build [-r [run mode]] [import path] [target path] ",
|
||||
Short: "build a Revel application (e.g. for deployment)",
|
||||
Long: `
|
||||
Build the Revel web application named by the given import path.
|
||||
This allows it to be deployed and run on a machine that lacks a Go installation.
|
||||
|
||||
For example:
|
||||
|
||||
revel build github.com/revel/examples/chat /tmp/chat
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdBuild.RunWith = buildApp
|
||||
cmdBuild.UpdateConfig = updateBuildConfig
|
||||
}
|
||||
|
||||
// The update config updates the configuration command so that it can run
|
||||
func updateBuildConfig(c *model.CommandConfig, args []string) bool {
|
||||
c.Index = model.BUILD
|
||||
if c.Build.TargetPath == "" {
|
||||
c.Build.TargetPath = "target"
|
||||
}
|
||||
if len(args) == 0 && c.Build.ImportPath != "" {
|
||||
return true
|
||||
}
|
||||
// If arguments were passed in then there must be two
|
||||
if len(args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "%s\n%s", cmdBuild.UsageLine, cmdBuild.Long)
|
||||
return false
|
||||
}
|
||||
|
||||
c.Build.ImportPath = args[0]
|
||||
c.Build.TargetPath = args[1]
|
||||
if len(args) > 2 {
|
||||
c.Build.Mode = args[2]
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// The main entry point to build application from command line
|
||||
func buildApp(c *model.CommandConfig) (err error) {
|
||||
|
||||
appImportPath, destPath, mode := c.ImportPath, c.Build.TargetPath, DefaultRunMode
|
||||
if len(c.Build.Mode) > 0 {
|
||||
mode = c.Build.Mode
|
||||
}
|
||||
|
||||
// Convert target to absolute path
|
||||
c.Build.TargetPath, _ = filepath.Abs(destPath)
|
||||
c.Build.Mode = mode
|
||||
c.Build.ImportPath = appImportPath
|
||||
|
||||
revel_paths, err := model.NewRevelPaths(mode, appImportPath, c.AppPath, model.NewWrappedRevelCallback(nil, c.PackageResolver))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = buildSafetyCheck(destPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the application can be built, this generates the main file
|
||||
app, err := harness.Build(c, revel_paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Copy files
|
||||
// Included are:
|
||||
// - run scripts
|
||||
// - binary
|
||||
// - revel
|
||||
// - app
|
||||
|
||||
return // 改了这里
|
||||
|
||||
packageFolders, err := buildCopyFiles(c, app, revel_paths)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = buildCopyModules(c, revel_paths, packageFolders, app)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = buildWriteScripts(c, app)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Copy the files to the target
|
||||
func buildCopyFiles(c *model.CommandConfig, app *harness.App, revel_paths *model.RevelContainer) (packageFolders []string, err error) {
|
||||
appImportPath, destPath := c.ImportPath, c.Build.TargetPath
|
||||
|
||||
// Revel and the app are in a directory structure mirroring import path
|
||||
srcPath := filepath.Join(destPath, "src")
|
||||
destBinaryPath := filepath.Join(destPath, filepath.Base(app.BinaryPath))
|
||||
tmpRevelPath := filepath.Join(srcPath, filepath.FromSlash(model.RevelImportPath))
|
||||
if err = utils.CopyFile(destBinaryPath, filepath.Join(revel_paths.BasePath, app.BinaryPath)); err != nil {
|
||||
return
|
||||
}
|
||||
utils.MustChmod(destBinaryPath, 0755)
|
||||
|
||||
// Copy the templates from the revel
|
||||
if err = utils.CopyDir(filepath.Join(tmpRevelPath, "conf"), filepath.Join(revel_paths.RevelPath, "conf"), nil); err != nil {
|
||||
return
|
||||
}
|
||||
if err = utils.CopyDir(filepath.Join(tmpRevelPath, "templates"), filepath.Join(revel_paths.RevelPath, "templates"), nil); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the folders to be packaged
|
||||
packageFolders = strings.Split(revel_paths.Config.StringDefault("package.folders", "conf,public,app/views"), ",")
|
||||
for i, p := range packageFolders {
|
||||
// Clean spaces, reformat slash to filesystem
|
||||
packageFolders[i] = filepath.FromSlash(strings.TrimSpace(p))
|
||||
}
|
||||
|
||||
if c.Build.CopySource {
|
||||
err = utils.CopyDir(filepath.Join(srcPath, filepath.FromSlash(appImportPath)), revel_paths.BasePath, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for _, folder := range packageFolders {
|
||||
err = utils.CopyDir(
|
||||
filepath.Join(srcPath, filepath.FromSlash(appImportPath), folder),
|
||||
filepath.Join(revel_paths.BasePath, folder),
|
||||
nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Based on the section copy over the build modules
|
||||
func buildCopyModules(c *model.CommandConfig, revel_paths *model.RevelContainer, packageFolders []string, app *harness.App) (err error) {
|
||||
destPath := filepath.Join(c.Build.TargetPath, "src")
|
||||
// Find all the modules used and copy them over.
|
||||
config := revel_paths.Config.Raw()
|
||||
|
||||
// We should only copy over the section of options what the build is targeted for
|
||||
// We will default to prod
|
||||
moduleImportList := []string{}
|
||||
for _, section := range config.Sections() {
|
||||
// If the runmode is defined we will only import modules defined for that run mode
|
||||
if c.Build.Mode != "" && c.Build.Mode != section {
|
||||
continue
|
||||
}
|
||||
options, _ := config.SectionOptions(section)
|
||||
for _, key := range options {
|
||||
if !strings.HasPrefix(key, "module.") {
|
||||
continue
|
||||
}
|
||||
moduleImportPath, _ := config.String(section, key)
|
||||
if moduleImportPath == "" {
|
||||
continue
|
||||
}
|
||||
moduleImportList = append(moduleImportList, moduleImportPath)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the the paths for each of the modules
|
||||
for _, importPath := range moduleImportList {
|
||||
fsPath := app.PackagePathMap[importPath]
|
||||
utils.Logger.Info("Copy files ", "to", filepath.Join(destPath, importPath), "from", fsPath)
|
||||
if c.Build.CopySource {
|
||||
err = utils.CopyDir(filepath.Join(destPath, importPath), fsPath, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for _, folder := range packageFolders {
|
||||
err = utils.CopyDir(
|
||||
filepath.Join(destPath, importPath, folder),
|
||||
filepath.Join(fsPath, folder),
|
||||
nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Write the run scripts for the build
|
||||
func buildWriteScripts(c *model.CommandConfig, app *harness.App) (err error) {
|
||||
tmplData := map[string]interface{}{
|
||||
"BinName": filepath.Base(app.BinaryPath),
|
||||
"ImportPath": c.Build.ImportPath,
|
||||
"Mode": c.Build.Mode,
|
||||
}
|
||||
|
||||
err = utils.GenerateTemplate(
|
||||
filepath.Join(c.Build.TargetPath, "run.sh"),
|
||||
PACKAGE_RUN_SH,
|
||||
tmplData,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
utils.MustChmod(filepath.Join(c.Build.TargetPath, "run.sh"), 0755)
|
||||
err = utils.GenerateTemplate(
|
||||
filepath.Join(c.Build.TargetPath, "run.bat"),
|
||||
PACKAGE_RUN_BAT,
|
||||
tmplData,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Your application has been built in:", c.Build.TargetPath)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Checks to see if the target folder exists and can be created
|
||||
func buildSafetyCheck(destPath string) error {
|
||||
|
||||
// First, verify that it is either already empty or looks like a previous
|
||||
// build (to avoid clobbering anything)
|
||||
if utils.Exists(destPath) && !utils.Empty(destPath) && !utils.Exists(filepath.Join(destPath, "run.sh")) {
|
||||
return utils.NewBuildError("Abort: %s exists and does not look like a build directory.", "path", destPath)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(destPath); err != nil && !os.IsNotExist(err) {
|
||||
return utils.NewBuildIfError(err, "Remove all error", "path", destPath)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(destPath, 0777); err != nil {
|
||||
return utils.NewBuildIfError(err, "MkDir all error", "path", destPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const PACKAGE_RUN_SH = `#!/bin/sh
|
||||
|
||||
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
|
||||
"$SCRIPTPATH/{{.BinName}}" -importPath {{.ImportPath}} -srcPath "$SCRIPTPATH/src" -runMode {{.Mode}}
|
||||
`
|
||||
const PACKAGE_RUN_BAT = `@echo off
|
||||
|
||||
{{.BinName}} -importPath {{.ImportPath}} -srcPath "%CD%\src" -runMode {{.Mode}}
|
||||
`
|
||||
4
app/cmd/gen_tmp.sh
Normal file
4
app/cmd/gen_tmp.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
|
||||
cd $SCRIPTPATH
|
||||
go run . build -v ../../ ./tmptmp
|
||||
rm -rf ./tmptmp
|
||||
219
app/cmd/harness/app.go
Normal file
219
app/cmd/harness/app.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
||||
// Revel Framework source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package harness
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
"sync"
|
||||
|
||||
"github.com/revel/cmd/model"
|
||||
"github.com/revel/cmd/utils"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// App contains the configuration for running a Revel app. (Not for the app itself)
|
||||
// Its only purpose is constructing the command to execute.
|
||||
type App struct {
|
||||
BinaryPath string // Path to the app executable
|
||||
Port int // Port to pass as a command line argument.
|
||||
cmd AppCmd // The last cmd returned.
|
||||
PackagePathMap map[string]string // Package to directory path map
|
||||
Paths *model.RevelContainer
|
||||
}
|
||||
|
||||
// NewApp returns app instance with binary path in it
|
||||
func NewApp(binPath string, paths *model.RevelContainer, packagePathMap map[string]string) *App {
|
||||
return &App{BinaryPath: binPath, Paths: paths, Port: paths.HTTPPort, PackagePathMap:packagePathMap}
|
||||
}
|
||||
|
||||
// Cmd returns a command to run the app server using the current configuration.
|
||||
func (a *App) Cmd(runMode string) AppCmd {
|
||||
a.cmd = NewAppCmd(a.BinaryPath, a.Port, runMode, a.Paths)
|
||||
return a.cmd
|
||||
}
|
||||
|
||||
// Kill the last app command returned.
|
||||
func (a *App) Kill() {
|
||||
a.cmd.Kill()
|
||||
}
|
||||
|
||||
// AppCmd manages the running of a Revel app server.
|
||||
// It requires revel.Init to have been called previously.
|
||||
type AppCmd struct {
|
||||
*exec.Cmd
|
||||
}
|
||||
|
||||
// NewAppCmd returns the AppCmd with parameters initialized for running app
|
||||
func NewAppCmd(binPath string, port int, runMode string, paths *model.RevelContainer) AppCmd {
|
||||
cmd := exec.Command(binPath,
|
||||
fmt.Sprintf("-port=%d", port),
|
||||
fmt.Sprintf("-importPath=%s", paths.ImportPath),
|
||||
fmt.Sprintf("-runMode=%s", runMode))
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
return AppCmd{cmd}
|
||||
}
|
||||
|
||||
// Start the app server, and wait until it is ready to serve requests.
|
||||
func (cmd AppCmd) Start(c *model.CommandConfig) error {
|
||||
listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool), c, &bytes.Buffer{}}
|
||||
cmd.Stdout = listeningWriter
|
||||
utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath)
|
||||
utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args, "dir", cmd.Dir, "env", cmd.Env)
|
||||
if err := cmd.Cmd.Start(); err != nil {
|
||||
utils.Logger.Fatal("Error running:", "error", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case exitState := <-cmd.waitChan():
|
||||
fmt.Println("Startup failure view previous messages, \n Proxy is listening :", c.Run.Port)
|
||||
err := utils.NewError("", "Revel Run Error", "starting your application there was an exception. See terminal output, " + exitState, "")
|
||||
// TODO pretiffy command line output
|
||||
// err.MetaError = listeningWriter.getLastOutput()
|
||||
return err
|
||||
|
||||
case <-time.After(60 * time.Second):
|
||||
println("Revel proxy is listening, point your browser to :", c.Run.Port)
|
||||
utils.Logger.Error("Killing revel server process did not respond after wait timeout.", "processid", cmd.Process.Pid)
|
||||
cmd.Kill()
|
||||
return errors.New("revel/harness: app timed out")
|
||||
|
||||
case <-listeningWriter.notifyReady:
|
||||
println("Revel proxy is listening, point your browser to :", c.Run.Port)
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Run the app server inline. Never returns.
|
||||
func (cmd AppCmd) Run(c *model.CommandConfig) {
|
||||
utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath)
|
||||
utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args)
|
||||
if err := cmd.Cmd.Run(); err != nil {
|
||||
utils.Logger.Fatal("Error running:", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Kill terminates the app server if it's running.
|
||||
func (cmd AppCmd) Kill() {
|
||||
|
||||
if cmd.Cmd != nil && (cmd.ProcessState == nil || !cmd.ProcessState.Exited()) {
|
||||
// Windows appears to send the kill to all threads, shutting down the
|
||||
// server before this can, this check will ensure the process is still running
|
||||
if _, err := os.FindProcess(int(cmd.Process.Pid)); err != nil {
|
||||
// Server has already exited
|
||||
utils.Logger.Info("Server not running revel server pid", "pid", cmd.Process.Pid)
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for the shutdown channel
|
||||
waitMutex := &sync.WaitGroup{}
|
||||
waitMutex.Add(1)
|
||||
ch := make(chan bool, 1)
|
||||
go func() {
|
||||
waitMutex.Done()
|
||||
s, err := cmd.Process.Wait()
|
||||
defer func() {
|
||||
ch <- true
|
||||
}()
|
||||
if err != nil {
|
||||
utils.Logger.Info("Wait failed for process ", "error", err)
|
||||
}
|
||||
if s != nil {
|
||||
utils.Logger.Info("Revel App exited", "state", s.String())
|
||||
}
|
||||
}()
|
||||
// Wait for the channel to begin waiting
|
||||
waitMutex.Wait()
|
||||
|
||||
// Send an interrupt signal to allow for a graceful shutdown
|
||||
utils.Logger.Info("Killing revel server pid", "pid", cmd.Process.Pid)
|
||||
var err error
|
||||
if runtime.GOOS != "windows" {
|
||||
// os.Interrupt is not available on windows
|
||||
err = cmd.Process.Signal(os.Interrupt)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.Logger.Info(
|
||||
"Revel app already exited.",
|
||||
"processid", cmd.Process.Pid, "error", err,
|
||||
"killerror", cmd.Process.Kill())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Use a timer to ensure that the process exits
|
||||
utils.Logger.Info("Waiting to exit")
|
||||
select {
|
||||
case <-ch:
|
||||
return
|
||||
case <-time.After(60 * time.Second):
|
||||
// Kill the process
|
||||
utils.Logger.Error(
|
||||
"Revel app failed to exit in 60 seconds - killing.",
|
||||
"processid", cmd.Process.Pid,
|
||||
"killerror", cmd.Process.Kill())
|
||||
}
|
||||
|
||||
utils.Logger.Info("Done Waiting to exit")
|
||||
}
|
||||
}
|
||||
|
||||
// Return a channel that is notified when Wait() returns.
|
||||
func (cmd AppCmd) waitChan() <-chan string {
|
||||
ch := make(chan string, 1)
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
state := cmd.ProcessState
|
||||
exitStatus := " unknown "
|
||||
if state != nil {
|
||||
exitStatus = state.String()
|
||||
}
|
||||
|
||||
ch <- exitStatus
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// A io.Writer that copies to the destination, and listens for "Revel engine is listening on.."
|
||||
// in the stream. (Which tells us when the revel server has finished starting up)
|
||||
// This is super ghetto, but by far the simplest thing that should work.
|
||||
type startupListeningWriter struct {
|
||||
dest io.Writer
|
||||
notifyReady chan bool
|
||||
c *model.CommandConfig
|
||||
buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
// Writes to this output stream
|
||||
func (w *startupListeningWriter) Write(p []byte) (int, error) {
|
||||
if w.notifyReady != nil && bytes.Contains(p, []byte("Revel engine is listening on")) {
|
||||
w.notifyReady <- true
|
||||
w.notifyReady = nil
|
||||
}
|
||||
if w.c.HistoricMode {
|
||||
if w.notifyReady != nil && bytes.Contains(p, []byte("Listening on")) {
|
||||
w.notifyReady <- true
|
||||
w.notifyReady = nil
|
||||
}
|
||||
}
|
||||
if w.notifyReady != nil {
|
||||
w.buffer.Write(p)
|
||||
}
|
||||
return w.dest.Write(p)
|
||||
}
|
||||
|
||||
// Returns the cleaned output from the response
|
||||
// TODO clean the response more
|
||||
func (w *startupListeningWriter) getLastOutput() string {
|
||||
return w.buffer.String()
|
||||
}
|
||||
|
||||
566
app/cmd/harness/build.go
Normal file
566
app/cmd/harness/build.go
Normal file
@@ -0,0 +1,566 @@
|
||||
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
||||
// Revel Framework source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package harness
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leanote/leanote/app/cmd/parser2"
|
||||
"github.com/revel/cmd/model"
|
||||
"github.com/revel/cmd/parser"
|
||||
_ "github.com/revel/cmd/parser"
|
||||
"github.com/revel/cmd/utils"
|
||||
)
|
||||
|
||||
var importErrorPattern = regexp.MustCompile("cannot find package \"([^\"]+)\"")
|
||||
|
||||
type ByString []*model.TypeInfo
|
||||
|
||||
func (c ByString) Len() int {
|
||||
return len(c)
|
||||
}
|
||||
func (c ByString) Swap(i, j int) {
|
||||
c[i], c[j] = c[j], c[i]
|
||||
}
|
||||
func (c ByString) Less(i, j int) bool {
|
||||
return c[i].String() < c[j].String()
|
||||
}
|
||||
|
||||
// Build the app:
|
||||
// 1. Generate the the main.go file.
|
||||
// 2. Run the appropriate "go build" command.
|
||||
// Requires that revel.Init has been called previously.
|
||||
// Returns the path to the built binary, and an error if there was a problem building it.
|
||||
func Build(c *model.CommandConfig, paths *model.RevelContainer) (_ *App, err error) {
|
||||
// First, clear the generated files (to avoid them messing with ProcessSource).
|
||||
cleanSource(paths, "tmp", "routes")
|
||||
|
||||
var sourceInfo *model.SourceInfo
|
||||
|
||||
if c.HistoricBuildMode {
|
||||
sourceInfo, err = parser.ProcessSource(paths)
|
||||
} else {
|
||||
sourceInfo, err = parser2.ProcessSource(paths)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add the db.import to the import paths.
|
||||
if dbImportPath, found := paths.Config.String("db.import"); found {
|
||||
sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, strings.Split(dbImportPath, ",")...)
|
||||
}
|
||||
|
||||
// Sort controllers so that file generation is reproducible
|
||||
controllers := sourceInfo.ControllerSpecs()
|
||||
sort.Stable(ByString(controllers))
|
||||
|
||||
// Generate two source files.
|
||||
templateArgs := map[string]interface{}{
|
||||
"ImportPath": paths.ImportPath,
|
||||
"Controllers": controllers,
|
||||
"ValidationKeys": sourceInfo.ValidationKeys,
|
||||
"ImportPaths": calcImportAliases(sourceInfo),
|
||||
"TestSuites": sourceInfo.TestSuites(),
|
||||
}
|
||||
|
||||
// Generate code for the main, run and routes file.
|
||||
// The run file allows external programs to launch and run the application
|
||||
// without being the main thread
|
||||
cleanSource(paths, "tmp", "routes")
|
||||
|
||||
if err = genSource(paths, "tmp", "main.go", RevelMainTemplate, templateArgs); err != nil {
|
||||
return
|
||||
}
|
||||
if err = genSource(paths, filepath.Join("tmp", "run"), "run.go", RevelRunTemplate, templateArgs); err != nil {
|
||||
return
|
||||
}
|
||||
if err = genSource(paths, "routes", "routes.go", RevelRoutesTemplate, templateArgs); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Logger.Warn("gen tmp/main.go, tmp/run/run.go, routes/routes.go success!!")
|
||||
|
||||
return // 改了这里
|
||||
|
||||
// Read build config.
|
||||
buildTags := paths.Config.StringDefault("build.tags", "")
|
||||
|
||||
// Build the user program (all code under app).
|
||||
// It relies on the user having "go" installed.
|
||||
goPath, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
utils.Logger.Fatal("Go executable not found in PATH.")
|
||||
}
|
||||
|
||||
// Binary path is a combination of target/app directory, app's import path and its name.
|
||||
binName := filepath.Join("target", "app", paths.ImportPath, filepath.Base(paths.BasePath))
|
||||
|
||||
// Change binary path for Windows build
|
||||
goos := runtime.GOOS
|
||||
if goosEnv := os.Getenv("GOOS"); goosEnv != "" {
|
||||
goos = goosEnv
|
||||
}
|
||||
if goos == "windows" {
|
||||
binName += ".exe"
|
||||
}
|
||||
|
||||
gotten := make(map[string]struct{})
|
||||
contains := func(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if len(c.GoModFlags) > 0 {
|
||||
for _, gomod := range c.GoModFlags {
|
||||
goModCmd := exec.Command(goPath, append([]string{"mod"}, strings.Split(gomod, " ")...)...)
|
||||
utils.CmdInit(goModCmd, !c.Vendored, c.AppPath)
|
||||
output, err := goModCmd.CombinedOutput()
|
||||
utils.Logger.Info("Gomod applied ", "output", string(output))
|
||||
|
||||
// If the build succeeded, we're done.
|
||||
if err != nil {
|
||||
utils.Logger.Error("Gomod Failed continuing ", "error", err, "output", string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
appVersion := getAppVersion(paths)
|
||||
if appVersion == "" {
|
||||
appVersion = "noVersionProvided"
|
||||
}
|
||||
|
||||
buildTime := time.Now().UTC().Format(time.RFC3339)
|
||||
versionLinkerFlags := fmt.Sprintf("-X '%s/app.AppVersion=%s' -X '%s/app.BuildTime=%s'",
|
||||
paths.ImportPath, appVersion, paths.ImportPath, buildTime)
|
||||
|
||||
// Append any build flags specified, they will override existing flags
|
||||
flags := []string{}
|
||||
if len(c.BuildFlags) == 0 {
|
||||
flags = []string{
|
||||
"build",
|
||||
"-ldflags", versionLinkerFlags,
|
||||
"-tags", buildTags,
|
||||
"-o", binName}
|
||||
} else {
|
||||
if !contains(c.BuildFlags, "build") {
|
||||
flags = []string{"build"}
|
||||
}
|
||||
if !contains(flags, "-ldflags") {
|
||||
ldflags := "-ldflags= " + versionLinkerFlags
|
||||
// Add user defined build flags
|
||||
for i := range c.BuildFlags {
|
||||
ldflags += " -X '" + c.BuildFlags[i] + "'"
|
||||
}
|
||||
flags = append(flags, ldflags)
|
||||
}
|
||||
if !contains(flags, "-tags") && buildTags != "" {
|
||||
flags = append(flags, "-tags", buildTags)
|
||||
}
|
||||
if !contains(flags, "-o") {
|
||||
flags = append(flags, "-o", binName)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: It's not applicable for filepath.* usage
|
||||
flags = append(flags, path.Join(paths.ImportPath, "app", "tmp"))
|
||||
|
||||
buildCmd := exec.Command(goPath, flags...)
|
||||
if !c.Vendored {
|
||||
// This is Go main path
|
||||
gopath := c.GoPath
|
||||
for _, o := range paths.ModulePathMap {
|
||||
gopath += string(filepath.ListSeparator) + o.Path
|
||||
}
|
||||
|
||||
buildCmd.Env = append(os.Environ(),
|
||||
"GOPATH=" + gopath,
|
||||
)
|
||||
}
|
||||
utils.CmdInit(buildCmd, !c.Vendored, c.AppPath)
|
||||
|
||||
utils.Logger.Info("Exec:", "args", buildCmd.Args, "working dir", buildCmd.Dir)
|
||||
output, err := buildCmd.CombinedOutput()
|
||||
|
||||
// If the build succeeded, we're done.
|
||||
if err == nil {
|
||||
utils.Logger.Info("Build successful continuing")
|
||||
return NewApp(binName, paths, sourceInfo.PackageMap), nil
|
||||
}
|
||||
|
||||
// Since there was an error, capture the output in case we need to report it
|
||||
stOutput := string(output)
|
||||
utils.Logger.Infof("Got error on build of app %s", stOutput)
|
||||
|
||||
// See if it was an import error that we can go get.
|
||||
matches := importErrorPattern.FindAllStringSubmatch(stOutput, -1)
|
||||
utils.Logger.Info("Build failed checking for missing imports", "message", stOutput, "missing_imports", len(matches))
|
||||
if matches == nil {
|
||||
utils.Logger.Info("Build failed no missing imports", "message", stOutput)
|
||||
return nil, newCompileError(paths, output)
|
||||
}
|
||||
utils.Logger.Warn("Detected missing packages, importing them", "packages", len(matches))
|
||||
for _, match := range matches {
|
||||
// Ensure we haven't already tried to go get it.
|
||||
pkgName := match[1]
|
||||
utils.Logger.Info("Trying to import ", "package", pkgName)
|
||||
if _, alreadyTried := gotten[pkgName]; alreadyTried {
|
||||
utils.Logger.Error("Failed to import ", "package", pkgName)
|
||||
return nil, newCompileError(paths, output)
|
||||
}
|
||||
gotten[pkgName] = struct{}{}
|
||||
if err := c.PackageResolver(pkgName); err != nil {
|
||||
utils.Logger.Error("Unable to resolve package", "package", pkgName, "error", err)
|
||||
return nil, newCompileError(paths, []byte(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// Success getting the import, attempt to build again.
|
||||
}
|
||||
|
||||
// TODO remove this unreachable code and document it
|
||||
utils.Logger.Fatal("Not reachable")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Try to define a version string for the compiled app
|
||||
// The following is tried (first match returns):
|
||||
// - Read a version explicitly specified in the APP_VERSION environment
|
||||
// variable
|
||||
// - Read the output of "git describe" if the source is in a git repository
|
||||
// If no version can be determined, an empty string is returned.
|
||||
func getAppVersion(paths *model.RevelContainer) string {
|
||||
if version := os.Getenv("APP_VERSION"); version != "" {
|
||||
return version
|
||||
}
|
||||
|
||||
// Check for the git binary
|
||||
if gitPath, err := exec.LookPath("git"); err == nil {
|
||||
// Check for the .git directory
|
||||
gitDir := filepath.Join(paths.BasePath, ".git")
|
||||
info, err := os.Stat(gitDir)
|
||||
if (err != nil && os.IsNotExist(err)) || !info.IsDir() {
|
||||
return ""
|
||||
}
|
||||
gitCmd := exec.Command(gitPath, "--git-dir=" + gitDir, "--work-tree=" + paths.BasePath, "describe", "--always", "--dirty")
|
||||
utils.Logger.Info("Exec:", "args", gitCmd.Args)
|
||||
output, err := gitCmd.Output()
|
||||
|
||||
if err != nil {
|
||||
utils.Logger.Error("Cannot determine git repository version:", "error", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return "git-" + strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func cleanSource(paths *model.RevelContainer, dirs ...string) {
|
||||
for _, dir := range dirs {
|
||||
cleanDir(paths, dir)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanDir(paths *model.RevelContainer, dir string) {
|
||||
utils.Logger.Info("Cleaning dir ", "dir", dir)
|
||||
tmpPath := filepath.Join(paths.AppPath, dir)
|
||||
f, err := os.Open(tmpPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
utils.Logger.Error("Failed to clean dir:", "error", err)
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
infos, err := f.Readdir(0)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
utils.Logger.Fatal("Failed to clean dir:", "error", err)
|
||||
}
|
||||
} else {
|
||||
for _, info := range infos {
|
||||
pathName := filepath.Join(tmpPath, info.Name())
|
||||
if info.IsDir() {
|
||||
err := os.RemoveAll(pathName)
|
||||
if err != nil {
|
||||
utils.Logger.Fatal("Failed to remove dir:", "error", err)
|
||||
}
|
||||
} else {
|
||||
err := os.Remove(pathName)
|
||||
if err != nil {
|
||||
utils.Logger.Fatal("Failed to remove file:", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// genSource renders the given template to produce source code, which it writes
|
||||
// to the given directory and file.
|
||||
func genSource(paths *model.RevelContainer, dir, filename, templateSource string, args map[string]interface{}) error {
|
||||
|
||||
return utils.GenerateTemplate(filepath.Join(paths.AppPath, dir, filename), templateSource, args)
|
||||
}
|
||||
|
||||
// Looks through all the method args and returns a set of unique import paths
|
||||
// that cover all the method arg types.
|
||||
// Additionally, assign package aliases when necessary to resolve ambiguity.
|
||||
func calcImportAliases(src *model.SourceInfo) map[string]string {
|
||||
aliases := make(map[string]string)
|
||||
typeArrays := [][]*model.TypeInfo{src.ControllerSpecs(), src.TestSuites()}
|
||||
for _, specs := range typeArrays {
|
||||
for _, spec := range specs {
|
||||
addAlias(aliases, spec.ImportPath, spec.PackageName)
|
||||
|
||||
for _, methSpec := range spec.MethodSpecs {
|
||||
for _, methArg := range methSpec.Args {
|
||||
if methArg.ImportPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
addAlias(aliases, methArg.ImportPath, methArg.TypeExpr.PkgName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the "InitImportPaths", with alias "_"
|
||||
for _, importPath := range src.InitImportPaths {
|
||||
if _, ok := aliases[importPath]; !ok {
|
||||
aliases[importPath] = "_"
|
||||
}
|
||||
}
|
||||
|
||||
return aliases
|
||||
}
|
||||
|
||||
// Adds an alias to the map of alias names
|
||||
func addAlias(aliases map[string]string, importPath, pkgName string) {
|
||||
alias, ok := aliases[importPath]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
alias = makePackageAlias(aliases, pkgName)
|
||||
aliases[importPath] = alias
|
||||
}
|
||||
|
||||
// Generates a package alias
|
||||
func makePackageAlias(aliases map[string]string, pkgName string) string {
|
||||
i := 0
|
||||
alias := pkgName
|
||||
for containsValue(aliases, alias) || alias == "revel" {
|
||||
alias = fmt.Sprintf("%s%d", pkgName, i)
|
||||
i++
|
||||
}
|
||||
return alias
|
||||
}
|
||||
|
||||
// Returns true if this value is in the map
|
||||
func containsValue(m map[string]string, val string) bool {
|
||||
for _, v := range m {
|
||||
if v == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the output of the "go build" command.
|
||||
// Return a detailed Error.
|
||||
func newCompileError(paths *model.RevelContainer, output []byte) *utils.SourceError {
|
||||
errorMatch := regexp.MustCompile(`(?m)^([^:#]+):(\d+):(\d+:)? (.*)$`).
|
||||
FindSubmatch(output)
|
||||
if errorMatch == nil {
|
||||
errorMatch = regexp.MustCompile(`(?m)^(.*?):(\d+):\s(.*?)$`).FindSubmatch(output)
|
||||
|
||||
if errorMatch == nil {
|
||||
utils.Logger.Error("Failed to parse build errors", "error", string(output))
|
||||
return &utils.SourceError{
|
||||
SourceType: "Go code",
|
||||
Title: "Go Compilation Error",
|
||||
Description: "See console for build error.",
|
||||
}
|
||||
}
|
||||
|
||||
errorMatch = append(errorMatch, errorMatch[3])
|
||||
|
||||
utils.Logger.Error("Build errors", "errors", string(output))
|
||||
}
|
||||
|
||||
findInPaths := func(relFilename string) string {
|
||||
// Extract the paths from the gopaths, and search for file there first
|
||||
gopaths := filepath.SplitList(build.Default.GOPATH)
|
||||
for _, gp := range gopaths {
|
||||
newPath := filepath.Join(gp, "src", paths.ImportPath, relFilename)
|
||||
println(newPath)
|
||||
if utils.Exists(newPath) {
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
newPath, _ := filepath.Abs(relFilename)
|
||||
utils.Logger.Warn("Could not find in GO path", "file", relFilename)
|
||||
return newPath
|
||||
}
|
||||
|
||||
|
||||
// Read the source for the offending file.
|
||||
var (
|
||||
relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go"
|
||||
absFilename = findInPaths(relFilename)
|
||||
line, _ = strconv.Atoi(string(errorMatch[2]))
|
||||
description = string(errorMatch[4])
|
||||
compileError = &utils.SourceError{
|
||||
SourceType: "Go code",
|
||||
Title: "Go Compilation Error",
|
||||
Path: relFilename,
|
||||
Description: description,
|
||||
Line: line,
|
||||
}
|
||||
)
|
||||
|
||||
errorLink := paths.Config.StringDefault("error.link", "")
|
||||
|
||||
if errorLink != "" {
|
||||
compileError.SetLink(errorLink)
|
||||
}
|
||||
|
||||
fileStr, err := utils.ReadLines(absFilename)
|
||||
if err != nil {
|
||||
compileError.MetaError = absFilename + ": " + err.Error()
|
||||
utils.Logger.Info("Unable to readlines " + compileError.MetaError, "error", err)
|
||||
return compileError
|
||||
}
|
||||
|
||||
compileError.SourceLines = fileStr
|
||||
return compileError
|
||||
}
|
||||
|
||||
// RevelMainTemplate template for app/tmp/run/run.go
|
||||
const RevelRunTemplate = `// GENERATED CODE - DO NOT EDIT
|
||||
// This file is the run file for Revel.
|
||||
// It registers all the controllers and provides details for the Revel server engine to
|
||||
// properly inject parameters directly into the action endpoints.
|
||||
package run
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"github.com/revel/revel"{{range $k, $v := $.ImportPaths}}
|
||||
{{$v}} "{{$k}}"{{end}}
|
||||
"github.com/revel/revel/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
// So compiler won't complain if the generated code doesn't reference reflect package...
|
||||
_ = reflect.Invalid
|
||||
)
|
||||
|
||||
// Register and run the application
|
||||
func Run(port int) {
|
||||
Register()
|
||||
revel.Run(port)
|
||||
}
|
||||
|
||||
// Register all the controllers
|
||||
func Register() {
|
||||
revel.AppLog.Info("Running revel server")
|
||||
{{range $i, $c := .Controllers}}
|
||||
revel.RegisterController((*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),
|
||||
[]*revel.MethodType{
|
||||
{{range .MethodSpecs}}&revel.MethodType{
|
||||
Name: "{{.Name}}",
|
||||
Args: []*revel.MethodArg{ {{range .Args}}
|
||||
&revel.MethodArg{Name: "{{.Name}}", Type: reflect.TypeOf((*{{index $.ImportPaths .ImportPath | .TypeExpr.TypeName}})(nil)) },{{end}}
|
||||
},
|
||||
RenderArgNames: map[int][]string{ {{range .RenderCalls}}
|
||||
{{.Line}}: []string{ {{range .Names}}
|
||||
"{{.}}",{{end}}
|
||||
},{{end}}
|
||||
},
|
||||
},
|
||||
{{end}}
|
||||
})
|
||||
{{end}}
|
||||
revel.DefaultValidationKeys = map[string]map[int]string{ {{range $path, $lines := .ValidationKeys}}
|
||||
"{{$path}}": { {{range $line, $key := $lines}}
|
||||
{{$line}}: "{{$key}}",{{end}}
|
||||
},{{end}}
|
||||
}
|
||||
testing.TestSuites = []interface{}{ {{range .TestSuites}}
|
||||
(*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),{{end}}
|
||||
}
|
||||
}
|
||||
`
|
||||
const RevelMainTemplate = `// GENERATED CODE - DO NOT EDIT
|
||||
// This file is the main file for Revel.
|
||||
// It registers all the controllers and provides details for the Revel server engine to
|
||||
// properly inject parameters directly into the action endpoints.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"{{.ImportPath}}/app/tmp/run"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var (
|
||||
runMode *string = flag.String("runMode", "", "Run mode.")
|
||||
port *int = flag.Int("port", 0, "By default, read from app.conf")
|
||||
importPath *string = flag.String("importPath", "", "Go Import Path for the app.")
|
||||
srcPath *string = flag.String("srcPath", "", "Path to the source root.")
|
||||
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
revel.Init(*runMode, *importPath, *srcPath)
|
||||
run.Run(*port)
|
||||
}
|
||||
`
|
||||
|
||||
// RevelRoutesTemplate template for app/conf/routes
|
||||
const RevelRoutesTemplate = `// GENERATED CODE - DO NOT EDIT
|
||||
// This file provides a way of creating URL's based on all the actions
|
||||
// found in all the controllers.
|
||||
package routes
|
||||
|
||||
import "github.com/revel/revel"
|
||||
|
||||
{{range $i, $c := .Controllers}}
|
||||
type t{{.StructName}} struct {}
|
||||
var {{.StructName}} t{{.StructName}}
|
||||
|
||||
{{range .MethodSpecs}}
|
||||
func (_ t{{$c.StructName}}) {{.Name}}({{range .Args}}
|
||||
{{.Name}} {{if .ImportPath}}interface{}{{else}}{{.TypeExpr.TypeName ""}}{{end}},{{end}}
|
||||
) string {
|
||||
args := make(map[string]string)
|
||||
{{range .Args}}
|
||||
revel.Unbind(args, "{{.Name}}", {{.Name}}){{end}}
|
||||
return revel.MainRouter.Reverse("{{$c.StructName}}.{{.Name}}", args).URL
|
||||
}
|
||||
{{end}}
|
||||
{{end}}
|
||||
`
|
||||
411
app/cmd/harness/harness.go
Normal file
411
app/cmd/harness/harness.go
Normal file
@@ -0,0 +1,411 @@
|
||||
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
||||
// Revel Framework source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package harness for a Revel Framework.
|
||||
//
|
||||
// It has a following responsibilities:
|
||||
// 1. Parse the user program, generating a main.go file that registers
|
||||
// controller classes and starts the user's server.
|
||||
// 2. Build and run the user program. Show compile errors.
|
||||
// 3. Monitor the user source and re-build / restart the program when necessary.
|
||||
//
|
||||
// Source files are generated in the app/tmp directory.
|
||||
package harness
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"time"
|
||||
"go/build"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/revel/cmd/model"
|
||||
"github.com/revel/cmd/utils"
|
||||
"github.com/revel/cmd/watcher"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
var (
|
||||
doNotWatch = []string{"tmp", "views", "routes"}
|
||||
|
||||
lastRequestHadError int32
|
||||
)
|
||||
|
||||
// Harness reverse proxies requests to the application server.
|
||||
// It builds / runs / rebuilds / restarts the server when code is changed.
|
||||
type Harness struct {
|
||||
app *App // The application
|
||||
useProxy bool // True if proxy is in use
|
||||
serverHost string // The proxy server host
|
||||
port int // The proxy serber port
|
||||
proxy *httputil.ReverseProxy // The proxy
|
||||
watcher *watcher.Watcher // The file watched
|
||||
mutex *sync.Mutex // A mutex to prevent concurrent updates
|
||||
paths *model.RevelContainer // The Revel container
|
||||
config *model.CommandConfig // The configuration
|
||||
runMode string // The runmode the harness is running in
|
||||
isError bool // True if harness is in error state
|
||||
ranOnce bool // True app compiled once
|
||||
}
|
||||
|
||||
func (h *Harness) renderError(iw http.ResponseWriter, ir *http.Request, err error) {
|
||||
// Render error here
|
||||
// Grab the template from three places
|
||||
// 1) Application/views/errors
|
||||
// 2) revel_home/views/errors
|
||||
// 3) views/errors
|
||||
if err == nil {
|
||||
utils.Logger.Panic("Caller passed in a nil error")
|
||||
}
|
||||
templateSet := template.New("__root__")
|
||||
seekViewOnPath := func(view string) (path string) {
|
||||
path = filepath.Join(h.paths.ViewsPath, "errors", view)
|
||||
if !utils.Exists(path) {
|
||||
path = filepath.Join(h.paths.RevelPath, "templates", "errors", view)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
utils.Logger.Error("Unable to read template file", path)
|
||||
}
|
||||
_, err = templateSet.New("errors/" + view).Parse(string(data))
|
||||
if err != nil {
|
||||
utils.Logger.Error("Unable to parse template file", path)
|
||||
}
|
||||
return
|
||||
}
|
||||
target := []string{seekViewOnPath("500.html"), seekViewOnPath("500-dev.html")}
|
||||
if !utils.Exists(target[0]) {
|
||||
fmt.Fprintf(iw, "Target template not found not found %s<br />\n", target[0])
|
||||
fmt.Fprintf(iw, "An error ocurred %s", err.Error())
|
||||
return
|
||||
}
|
||||
var revelError *utils.SourceError
|
||||
switch e := err.(type) {
|
||||
case *utils.SourceError:
|
||||
revelError = e
|
||||
case error:
|
||||
revelError = &utils.SourceError{
|
||||
Title: "Server Error",
|
||||
Description: e.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if revelError == nil {
|
||||
panic("no error provided")
|
||||
}
|
||||
viewArgs := map[string]interface{}{}
|
||||
viewArgs["RunMode"] = h.paths.RunMode
|
||||
viewArgs["DevMode"] = h.paths.DevMode
|
||||
viewArgs["Error"] = revelError
|
||||
|
||||
// Render the template from the file
|
||||
err = templateSet.ExecuteTemplate(iw, "errors/500.html", viewArgs)
|
||||
if err != nil {
|
||||
utils.Logger.Error("Failed to execute", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP handles all requests.
|
||||
// It checks for changes to app, rebuilds if necessary, and forwards the request.
|
||||
func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Don't rebuild the app for favicon requests.
|
||||
if lastRequestHadError > 0 && r.URL.Path == "/favicon.ico" {
|
||||
return
|
||||
}
|
||||
|
||||
// Flush any change events and rebuild app if necessary.
|
||||
// Render an error page if the rebuild / restart failed.
|
||||
err := h.watcher.Notify()
|
||||
if err != nil {
|
||||
// In a thread safe manner update the flag so that a request for
|
||||
// /favicon.ico does not trigger a rebuild
|
||||
atomic.CompareAndSwapInt32(&lastRequestHadError, 0, 1)
|
||||
h.renderError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// In a thread safe manner update the flag so that a request for
|
||||
// /favicon.ico is allowed
|
||||
atomic.CompareAndSwapInt32(&lastRequestHadError, 1, 0)
|
||||
|
||||
// Reverse proxy the request.
|
||||
// (Need special code for websockets, courtesy of bradfitz)
|
||||
if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
|
||||
h.proxyWebsocket(w, r, h.serverHost)
|
||||
} else {
|
||||
h.proxy.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// NewHarness method returns a reverse proxy that forwards requests
|
||||
// to the given port.
|
||||
func NewHarness(c *model.CommandConfig, paths *model.RevelContainer, runMode string, noProxy bool) *Harness {
|
||||
// Get a template loader to render errors.
|
||||
// Prefer the app's views/errors directory, and fall back to the stock error pages.
|
||||
//revel.MainTemplateLoader = revel.NewTemplateLoader(
|
||||
// []string{filepath.Join(revel.RevelPath, "templates")})
|
||||
//if err := revel.MainTemplateLoader.Refresh(); err != nil {
|
||||
// revel.RevelLog.Error("Template loader error", "error", err)
|
||||
//}
|
||||
|
||||
addr := paths.HTTPAddr
|
||||
port := paths.Config.IntDefault("harness.port", 0)
|
||||
scheme := "http"
|
||||
|
||||
if paths.HTTPSsl {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// If the server is running on the wildcard address, use "localhost"
|
||||
if addr == "" {
|
||||
utils.Logger.Warn("No http.addr specified in the app.conf listening on localhost interface only. " +
|
||||
"This will not allow external access to your application")
|
||||
addr = "localhost"
|
||||
}
|
||||
|
||||
if port == 0 {
|
||||
port = getFreePort()
|
||||
}
|
||||
|
||||
serverURL, _ := url.ParseRequestURI(fmt.Sprintf(scheme+"://%s:%d", addr, port))
|
||||
|
||||
serverHarness := &Harness{
|
||||
port: port,
|
||||
serverHost: serverURL.String()[len(scheme+"://"):],
|
||||
proxy: httputil.NewSingleHostReverseProxy(serverURL),
|
||||
mutex: &sync.Mutex{},
|
||||
paths: paths,
|
||||
useProxy: !noProxy,
|
||||
config: c,
|
||||
runMode: runMode,
|
||||
}
|
||||
|
||||
if paths.HTTPSsl {
|
||||
serverHarness.proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
}
|
||||
return serverHarness
|
||||
}
|
||||
|
||||
// Refresh method rebuilds the Revel application and run it on the given port.
|
||||
// called by the watcher
|
||||
func (h *Harness) Refresh() (err *utils.SourceError) {
|
||||
t := time.Now();
|
||||
fmt.Println("Changed detected, recompiling")
|
||||
err = h.refresh()
|
||||
if err!=nil && !h.ranOnce && h.useProxy {
|
||||
addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
|
||||
|
||||
fmt.Printf("\nError compiling code, to view error details see proxy running on http://%s\n\n",addr)
|
||||
}
|
||||
|
||||
h.ranOnce = true
|
||||
fmt.Printf("\nTime to recompile %s\n",time.Now().Sub(t).String())
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Harness) refresh() (err *utils.SourceError) {
|
||||
// Allow only one thread to rebuild the process
|
||||
// If multiple requests to rebuild are queued only the last one is executed on
|
||||
// So before a build is started we wait for a second to determine if
|
||||
// more requests for a build are triggered.
|
||||
// Once no more requests are triggered the build will be processed
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
if h.app != nil {
|
||||
h.app.Kill()
|
||||
}
|
||||
|
||||
utils.Logger.Info("Rebuild Called")
|
||||
var newErr error
|
||||
h.app, newErr = Build(h.config, h.paths)
|
||||
if newErr != nil {
|
||||
utils.Logger.Error("Build detected an error", "error", newErr)
|
||||
if castErr, ok := newErr.(*utils.SourceError); ok {
|
||||
return castErr
|
||||
}
|
||||
err = &utils.SourceError{
|
||||
Title: "App failed to start up",
|
||||
Description: err.Error(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if h.useProxy {
|
||||
h.app.Port = h.port
|
||||
runMode := h.runMode
|
||||
if !h.config.HistoricMode {
|
||||
// Recalulate run mode based on the config
|
||||
var paths []byte
|
||||
if len(h.app.PackagePathMap)>0 {
|
||||
paths, _ = json.Marshal(h.app.PackagePathMap)
|
||||
}
|
||||
runMode = fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v,"packagePathMap":%s}`, h.app.Paths.RunMode, h.config.Verbose, string(paths))
|
||||
|
||||
}
|
||||
if err2 := h.app.Cmd(runMode).Start(h.config); err2 != nil {
|
||||
utils.Logger.Error("Could not start application", "error", err2)
|
||||
if err,k :=err2.(*utils.SourceError);k {
|
||||
return err
|
||||
}
|
||||
return &utils.SourceError{
|
||||
Title: "App failed to start up",
|
||||
Description: err2.Error(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
h.app = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// WatchDir method returns false to file matches with doNotWatch
|
||||
// otheriwse true
|
||||
func (h *Harness) WatchDir(info os.FileInfo) bool {
|
||||
return !utils.ContainsString(doNotWatch, info.Name())
|
||||
}
|
||||
|
||||
// WatchFile method returns true given filename HasSuffix of ".go"
|
||||
// otheriwse false - implements revel.DiscerningListener
|
||||
func (h *Harness) WatchFile(filename string) bool {
|
||||
return strings.HasSuffix(filename, ".go")
|
||||
}
|
||||
|
||||
// Run the harness, which listens for requests and proxies them to the app
|
||||
// server, which it runs and rebuilds as necessary.
|
||||
func (h *Harness) Run() {
|
||||
var paths []string
|
||||
if h.paths.Config.BoolDefault("watch.gopath", false) {
|
||||
gopaths := filepath.SplitList(build.Default.GOPATH)
|
||||
paths = append(paths, gopaths...)
|
||||
}
|
||||
paths = append(paths, h.paths.CodePaths...)
|
||||
h.watcher = watcher.NewWatcher(h.paths, false)
|
||||
h.watcher.Listen(h, paths...)
|
||||
go h.Refresh()
|
||||
// h.watcher.Notify()
|
||||
|
||||
if h.useProxy {
|
||||
go func() {
|
||||
// Check the port to start on a random port
|
||||
if h.paths.HTTPPort == 0 {
|
||||
h.paths.HTTPPort = getFreePort()
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
|
||||
utils.Logger.Infof("Proxy server is listening on %s", addr)
|
||||
|
||||
|
||||
var err error
|
||||
if h.paths.HTTPSsl {
|
||||
err = http.ListenAndServeTLS(
|
||||
addr,
|
||||
h.paths.HTTPSslCert,
|
||||
h.paths.HTTPSslKey,
|
||||
h)
|
||||
} else {
|
||||
err = http.ListenAndServe(addr, h)
|
||||
}
|
||||
if err != nil {
|
||||
utils.Logger.Error("Failed to start reverse proxy:", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
// Make a new channel to listen for the interrupt event
|
||||
ch := make(chan os.Signal)
|
||||
signal.Notify(ch, os.Interrupt, os.Kill)
|
||||
<-ch
|
||||
// Kill the app and exit
|
||||
if h.app != nil {
|
||||
h.app.Kill()
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Find an unused port
|
||||
func getFreePort() (port int) {
|
||||
conn, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
utils.Logger.Fatal("Unable to fetch a freee port address", "error", err)
|
||||
}
|
||||
|
||||
port = conn.Addr().(*net.TCPAddr).Port
|
||||
err = conn.Close()
|
||||
if err != nil {
|
||||
utils.Logger.Fatal("Unable to close port", "error", err)
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// proxyWebsocket copies data between websocket client and server until one side
|
||||
// closes the connection. (ReverseProxy doesn't work with websocket requests.)
|
||||
func (h *Harness) proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
|
||||
var (
|
||||
d net.Conn
|
||||
err error
|
||||
)
|
||||
if h.paths.HTTPSsl {
|
||||
// since this proxy isn't used in production,
|
||||
// it's OK to set InsecureSkipVerify to true
|
||||
// no need to add another configuration option.
|
||||
d, err = tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true})
|
||||
} else {
|
||||
d, err = net.Dial("tcp", host)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "Error contacting backend server.", 500)
|
||||
utils.Logger.Error("Error dialing websocket backend ", "host", host, "error", err)
|
||||
return
|
||||
}
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "Not a hijacker?", 500)
|
||||
return
|
||||
}
|
||||
nc, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
utils.Logger.Error("Hijack error", "error", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err = nc.Close(); err != nil {
|
||||
utils.Logger.Error("Connection close error", "error", err)
|
||||
}
|
||||
if err = d.Close(); err != nil {
|
||||
utils.Logger.Error("Dial close error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = r.Write(d)
|
||||
if err != nil {
|
||||
utils.Logger.Error("Error copying request to target", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
errc := make(chan error, 2)
|
||||
cp := func(dst io.Writer, src io.Reader) {
|
||||
_, err := io.Copy(dst, src)
|
||||
errc <- err
|
||||
}
|
||||
go cp(d, nc)
|
||||
go cp(nc, d)
|
||||
<-errc
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/revel/cmd/harness"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// go run main.go
|
||||
// 生成routes.go, main.go
|
||||
revel.Init("", "github.com/leanote/leanote", "")
|
||||
_, err := harness.Build() // ok, err
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("Ok")
|
||||
// panicOnError(reverr, "Failed to build")
|
||||
}
|
||||
427
app/cmd/parser2/source_info_processor.go
Normal file
427
app/cmd/parser2/source_info_processor.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package parser2
|
||||
|
||||
import (
|
||||
"github.com/revel/cmd/utils"
|
||||
"golang.org/x/tools/go/packages"
|
||||
"github.com/revel/cmd/model"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
"github.com/revel/cmd/logger"
|
||||
)
|
||||
|
||||
type (
|
||||
SourceInfoProcessor struct {
|
||||
sourceProcessor *SourceProcessor
|
||||
}
|
||||
)
|
||||
|
||||
func NewSourceInfoProcessor(sourceProcessor *SourceProcessor) *SourceInfoProcessor {
|
||||
return &SourceInfoProcessor{sourceProcessor:sourceProcessor}
|
||||
}
|
||||
|
||||
func (s *SourceInfoProcessor) processPackage(p *packages.Package) (sourceInfo *model.SourceInfo) {
|
||||
sourceInfo = &model.SourceInfo{
|
||||
ValidationKeys: map[string]map[int]string{},
|
||||
}
|
||||
var (
|
||||
isController = strings.HasSuffix(p.PkgPath, "/controllers") ||
|
||||
strings.Contains(p.PkgPath, "/controllers/")
|
||||
isTest = strings.HasSuffix(p.PkgPath, "/tests") ||
|
||||
strings.Contains(p.PkgPath, "/tests/")
|
||||
methodMap = map[string][]*model.MethodSpec{}
|
||||
)
|
||||
localImportMap := map[string]string{}
|
||||
log := s.sourceProcessor.log.New("package", p.PkgPath)
|
||||
log.Info("Processing package")
|
||||
for _, tree := range p.Syntax {
|
||||
for _, decl := range tree.Decls {
|
||||
|
||||
s.sourceProcessor.packageMap[p.PkgPath] = filepath.Dir(p.Fset.Position(decl.Pos()).Filename)
|
||||
if !s.addImport(decl, p, localImportMap, log) {
|
||||
continue
|
||||
}
|
||||
spec, found := s.getStructTypeDecl(decl, p.Fset)
|
||||
//log.Info("Checking file","filename", p.Fset.Position(decl.Pos()).Filename,"found",found)
|
||||
if found {
|
||||
if isController || isTest {
|
||||
controllerSpec := s.getControllerSpec(spec, p, localImportMap)
|
||||
sourceInfo.StructSpecs = append(sourceInfo.StructSpecs, controllerSpec)
|
||||
}
|
||||
} else {
|
||||
// Not a type definition, this could be a method for a controller try to extract that
|
||||
// Func declaration?
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// This could be a controller action endpoint, check and add if needed
|
||||
if isController &&
|
||||
funcDecl.Recv != nil && // Must have a receiver
|
||||
funcDecl.Name.IsExported() && // be public
|
||||
funcDecl.Type.Results != nil && len(funcDecl.Type.Results.List) == 1 {
|
||||
// return one result
|
||||
if m, receiver := s.getControllerFunc(funcDecl, p, localImportMap); m != nil {
|
||||
methodMap[receiver] = append(methodMap[receiver], m)
|
||||
log.Info("Added method map to ", "receiver", receiver, "method", m.Name)
|
||||
}
|
||||
}
|
||||
// Check for validation
|
||||
if lineKeyMap := s.getValidation(funcDecl, p); len(lineKeyMap) > 1 {
|
||||
sourceInfo.ValidationKeys[p.PkgPath + "." + s.getFuncName(funcDecl)] = lineKeyMap
|
||||
}
|
||||
if funcDecl.Name.Name == "init" {
|
||||
sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, p.PkgPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the method specs to the struct specs.
|
||||
for _, spec := range sourceInfo.StructSpecs {
|
||||
spec.MethodSpecs = methodMap[spec.StructName]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// Scan app source code for calls to X.Y(), where X is of type *Validation.
|
||||
//
|
||||
// Recognize these scenarios:
|
||||
// - "Y" = "Validation" and is a member of the receiver.
|
||||
// (The common case for inline validation)
|
||||
// - "X" is passed in to the func as a parameter.
|
||||
// (For structs implementing Validated)
|
||||
//
|
||||
// The line number to which a validation call is attributed is that of the
|
||||
// surrounding ExprStmt. This is so that it matches what runtime.Callers()
|
||||
// reports.
|
||||
//
|
||||
// The end result is that we can set the default validation key for each call to
|
||||
// be the same as the local variable.
|
||||
func (s *SourceInfoProcessor) getValidation(funcDecl *ast.FuncDecl, p *packages.Package) (map[int]string) {
|
||||
var (
|
||||
lineKeys = make(map[int]string)
|
||||
|
||||
// Check the func parameters and the receiver's members for the *revel.Validation type.
|
||||
validationParam = s.getValidationParameter(funcDecl)
|
||||
)
|
||||
|
||||
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
|
||||
// e.g. c.Validation.Required(arg) or v.Required(arg)
|
||||
callExpr, ok := node.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// e.g. c.Validation.Required or v.Required
|
||||
funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
switch x := funcSelector.X.(type) {
|
||||
case *ast.SelectorExpr: // e.g. c.Validation
|
||||
if x.Sel.Name != "Validation" {
|
||||
return true
|
||||
}
|
||||
|
||||
case *ast.Ident: // e.g. v
|
||||
if validationParam == nil || x.Obj != validationParam {
|
||||
return true
|
||||
}
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
if len(callExpr.Args) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Given the validation expression, extract the key.
|
||||
key := callExpr.Args[0]
|
||||
switch expr := key.(type) {
|
||||
case *ast.BinaryExpr:
|
||||
// If the argument is a binary expression, take the first expression.
|
||||
// (e.g. c.Validation.Required(myName != ""))
|
||||
key = expr.X
|
||||
case *ast.UnaryExpr:
|
||||
// If the argument is a unary expression, drill in.
|
||||
// (e.g. c.Validation.Required(!myBool)
|
||||
key = expr.X
|
||||
case *ast.BasicLit:
|
||||
// If it's a literal, skip it.
|
||||
return true
|
||||
}
|
||||
|
||||
if typeExpr := model.NewTypeExprFromAst("", key); typeExpr.Valid {
|
||||
lineKeys[p.Fset.Position(callExpr.End()).Line] = typeExpr.TypeName("")
|
||||
} else {
|
||||
s.sourceProcessor.log.Error("Error: Failed to generate key for field validation. Make sure the field name is valid.", "file", p.PkgPath,
|
||||
"line", p.Fset.Position(callExpr.End()).Line, "function", funcDecl.Name.String())
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return lineKeys
|
||||
|
||||
}
|
||||
// Check to see if there is a *revel.Validation as an argument.
|
||||
func (s *SourceInfoProcessor) getValidationParameter(funcDecl *ast.FuncDecl) *ast.Object {
|
||||
for _, field := range funcDecl.Type.Params.List {
|
||||
starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if selExpr.Sel.Name == "Validation" && s.sourceProcessor.importMap[xIdent.Name] == model.RevelImportPath {
|
||||
return field.Names[0].Obj
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *SourceInfoProcessor) getControllerFunc(funcDecl *ast.FuncDecl, p *packages.Package, localImportMap map[string]string) (method *model.MethodSpec, recvTypeName string) {
|
||||
selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if selExpr.Sel.Name != "Result" {
|
||||
return
|
||||
}
|
||||
if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || s.sourceProcessor.importMap[pkgIdent.Name] != model.RevelImportPath {
|
||||
return
|
||||
}
|
||||
method = &model.MethodSpec{
|
||||
Name: funcDecl.Name.Name,
|
||||
}
|
||||
|
||||
// Add a description of the arguments to the method.
|
||||
for _, field := range funcDecl.Type.Params.List {
|
||||
for _, name := range field.Names {
|
||||
var importPath string
|
||||
typeExpr := model.NewTypeExprFromAst(p.Name, field.Type)
|
||||
if !typeExpr.Valid {
|
||||
utils.Logger.Warn("Warn: Didn't understand argument '%s' of action %s. Ignoring.", name, s.getFuncName(funcDecl))
|
||||
return // We didn't understand one of the args. Ignore this action.
|
||||
}
|
||||
// Local object
|
||||
if typeExpr.PkgName == p.Name {
|
||||
importPath = p.PkgPath
|
||||
} else if typeExpr.PkgName != "" {
|
||||
var ok bool
|
||||
if importPath, ok = localImportMap[typeExpr.PkgName]; !ok {
|
||||
if importPath, ok = s.sourceProcessor.importMap[typeExpr.PkgName]; !ok {
|
||||
utils.Logger.Error("Unable to find import", "importMap", s.sourceProcessor.importMap, "localimport", localImportMap)
|
||||
utils.Logger.Fatalf("Failed to find import for arg of type: %s , %s", typeExpr.PkgName, typeExpr.TypeName(""))
|
||||
}
|
||||
}
|
||||
}
|
||||
method.Args = append(method.Args, &model.MethodArg{
|
||||
Name: name.Name,
|
||||
TypeExpr: typeExpr,
|
||||
ImportPath: importPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add a description of the calls to Render from the method.
|
||||
// Inspect every node (e.g. always return true).
|
||||
method.RenderCalls = []*model.MethodCall{}
|
||||
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
|
||||
// Is it a function call?
|
||||
callExpr, ok := node.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// Is it calling (*Controller).Render?
|
||||
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// The type of the receiver is not easily available, so just store every
|
||||
// call to any method called Render.
|
||||
if selExpr.Sel.Name != "Render" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Add this call's args to the renderArgs.
|
||||
pos := p.Fset.Position(callExpr.Lparen)
|
||||
methodCall := &model.MethodCall{
|
||||
Line: pos.Line,
|
||||
Names: []string{},
|
||||
}
|
||||
for _, arg := range callExpr.Args {
|
||||
argIdent, ok := arg.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
methodCall.Names = append(methodCall.Names, argIdent.Name)
|
||||
}
|
||||
method.RenderCalls = append(method.RenderCalls, methodCall)
|
||||
return true
|
||||
})
|
||||
|
||||
var recvType = funcDecl.Recv.List[0].Type
|
||||
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
|
||||
recvTypeName = recvStarType.X.(*ast.Ident).Name
|
||||
} else {
|
||||
recvTypeName = recvType.(*ast.Ident).Name
|
||||
}
|
||||
return
|
||||
}
|
||||
func (s *SourceInfoProcessor) getControllerSpec(spec *ast.TypeSpec, p *packages.Package, localImportMap map[string]string) (controllerSpec *model.TypeInfo) {
|
||||
structType := spec.Type.(*ast.StructType)
|
||||
|
||||
// At this point we know it's a type declaration for a struct.
|
||||
// Fill in the rest of the info by diving into the fields.
|
||||
// Add it provisionally to the Controller list -- it's later filtered using field info.
|
||||
controllerSpec = &model.TypeInfo{
|
||||
StructName: spec.Name.Name,
|
||||
ImportPath: p.PkgPath,
|
||||
PackageName: p.Name,
|
||||
}
|
||||
log := s.sourceProcessor.log.New("file", p.Fset.Position(spec.Pos()).Filename, "position", p.Fset.Position(spec.Pos()).Line)
|
||||
for _, field := range structType.Fields.List {
|
||||
// If field.Names is set, it's not an embedded type.
|
||||
if field.Names != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// A direct "sub-type" has an ast.Field as either:
|
||||
// Ident { "AppController" }
|
||||
// SelectorExpr { "rev", "Controller" }
|
||||
// Additionally, that can be wrapped by StarExprs.
|
||||
fieldType := field.Type
|
||||
pkgName, typeName := func() (string, string) {
|
||||
// Drill through any StarExprs.
|
||||
for {
|
||||
if starExpr, ok := fieldType.(*ast.StarExpr); ok {
|
||||
fieldType = starExpr.X
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// If the embedded type is in the same package, it's an Ident.
|
||||
if ident, ok := fieldType.(*ast.Ident); ok {
|
||||
return "", ident.Name
|
||||
}
|
||||
|
||||
if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok {
|
||||
if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok {
|
||||
return pkgIdent.Name, selectorExpr.Sel.Name
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}()
|
||||
|
||||
// If a typename wasn't found, skip it.
|
||||
if typeName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the import path for this type.
|
||||
// If it was referenced without a package name, use the current package import path.
|
||||
// Else, look up the package's import path by name.
|
||||
var importPath string
|
||||
if pkgName == "" {
|
||||
importPath = p.PkgPath
|
||||
} else {
|
||||
var ok bool
|
||||
if importPath, ok = localImportMap[pkgName]; !ok {
|
||||
log.Debug("Debug: Unusual, failed to find package locally ", "package", pkgName, "type", typeName, "map", s.sourceProcessor.importMap, "usedin", )
|
||||
if importPath, ok = s.sourceProcessor.importMap[pkgName]; !ok {
|
||||
log.Error("Error: Failed to find import path for ", "package", pkgName, "type", typeName, "map", s.sourceProcessor.importMap, "usedin", )
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
controllerSpec.EmbeddedTypes = append(controllerSpec.EmbeddedTypes, &model.EmbeddedTypeName{
|
||||
ImportPath: importPath,
|
||||
StructName: typeName,
|
||||
})
|
||||
}
|
||||
s.sourceProcessor.log.Info("Added controller spec", "name", controllerSpec.StructName, "package", controllerSpec.ImportPath)
|
||||
return
|
||||
}
|
||||
func (s *SourceInfoProcessor) getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if genDecl.Tok != token.TYPE {
|
||||
return
|
||||
}
|
||||
|
||||
if len(genDecl.Specs) == 0 {
|
||||
utils.Logger.Warn("Warn: Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line)
|
||||
return
|
||||
}
|
||||
|
||||
spec = genDecl.Specs[0].(*ast.TypeSpec)
|
||||
_, found = spec.Type.(*ast.StructType)
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
func (s *SourceInfoProcessor) getFuncName(funcDecl *ast.FuncDecl) string {
|
||||
prefix := ""
|
||||
if funcDecl.Recv != nil {
|
||||
recvType := funcDecl.Recv.List[0].Type
|
||||
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
|
||||
prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")"
|
||||
} else {
|
||||
prefix = recvType.(*ast.Ident).Name
|
||||
}
|
||||
prefix += "."
|
||||
}
|
||||
return prefix + funcDecl.Name.Name
|
||||
}
|
||||
func (s *SourceInfoProcessor) addImport(decl ast.Decl, p *packages.Package, localImportMap map[string]string, log logger.MultiLogger) (shouldContinue bool) {
|
||||
shouldContinue = true
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if genDecl.Tok == token.IMPORT {
|
||||
shouldContinue = false
|
||||
for _, spec := range genDecl.Specs {
|
||||
importSpec := spec.(*ast.ImportSpec)
|
||||
//fmt.Printf("*** import specification %#v\n", importSpec)
|
||||
var pkgAlias string
|
||||
if importSpec.Name != nil {
|
||||
pkgAlias = importSpec.Name.Name
|
||||
if pkgAlias == "_" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\""
|
||||
fullPath := quotedPath[1 : len(quotedPath) - 1] // Remove the quotes
|
||||
if pkgAlias == "" {
|
||||
pkgAlias = fullPath
|
||||
if index := strings.LastIndex(pkgAlias, "/"); index > 0 {
|
||||
pkgAlias = pkgAlias[index + 1:]
|
||||
}
|
||||
}
|
||||
localImportMap[pkgAlias] = fullPath
|
||||
}
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
310
app/cmd/parser2/source_processor.go
Normal file
310
app/cmd/parser2/source_processor.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package parser2
|
||||
|
||||
import (
|
||||
"github.com/revel/cmd/logger"
|
||||
"github.com/revel/cmd/model"
|
||||
"github.com/revel/cmd/utils"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"golang.org/x/tools/go/packages"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
SourceProcessor struct {
|
||||
revelContainer *model.RevelContainer
|
||||
log logger.MultiLogger
|
||||
packageList []*packages.Package
|
||||
importMap map[string]string
|
||||
packageMap map[string]string
|
||||
sourceInfoProcessor *SourceInfoProcessor
|
||||
sourceInfo *model.SourceInfo
|
||||
}
|
||||
)
|
||||
|
||||
func ProcessSource(revelContainer *model.RevelContainer) (sourceInfo *model.SourceInfo, compileError error) {
|
||||
utils.Logger.Info("ProcessSource")
|
||||
processor := NewSourceProcessor(revelContainer)
|
||||
compileError = processor.parse()
|
||||
sourceInfo = processor.sourceInfo
|
||||
if compileError == nil {
|
||||
processor.log.Infof("From parsers : Structures:%d InitImports:%d ValidationKeys:%d %v", len(sourceInfo.StructSpecs), len(sourceInfo.InitImportPaths), len(sourceInfo.ValidationKeys), sourceInfo.PackageMap)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func NewSourceProcessor(revelContainer *model.RevelContainer) *SourceProcessor {
|
||||
s := &SourceProcessor{revelContainer:revelContainer, log:utils.Logger.New("parser", "SourceProcessor")}
|
||||
s.sourceInfoProcessor = NewSourceInfoProcessor(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SourceProcessor) parse() (compileError error) {
|
||||
print("Parsing packages, (may require download if not cached)...")
|
||||
if compileError = s.addPackages(); compileError != nil {
|
||||
return
|
||||
}
|
||||
println(" Completed")
|
||||
if compileError = s.addImportMap(); compileError != nil {
|
||||
return
|
||||
}
|
||||
if compileError = s.addSourceInfo(); compileError != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.sourceInfo.PackageMap = map[string]string{}
|
||||
getImportFromMap := func(packagePath string) string {
|
||||
for path := range s.packageMap {
|
||||
if strings.Index(path, packagePath) == 0 {
|
||||
fullPath := s.packageMap[path]
|
||||
return fullPath[:(len(fullPath) - len(path) + len(packagePath))]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
s.sourceInfo.PackageMap[model.RevelImportPath] = getImportFromMap(model.RevelImportPath)
|
||||
s.sourceInfo.PackageMap[s.revelContainer.ImportPath] = getImportFromMap(s.revelContainer.ImportPath)
|
||||
for _, module := range s.revelContainer.ModulePathMap {
|
||||
s.sourceInfo.PackageMap[module.ImportPath] = getImportFromMap(module.ImportPath)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 这两个方法来自util
|
||||
|
||||
// Shortcut to fsWalk
|
||||
func (s *SourceProcessor) Walk(root string, walkFn filepath.WalkFunc) error {
|
||||
return s.fsWalk(root, root, walkFn)
|
||||
}
|
||||
|
||||
// Walk the path tree using the function
|
||||
// Every file found will call the function
|
||||
func (s *SourceProcessor) fsWalk(fname string, linkName string, walkFn filepath.WalkFunc) error {
|
||||
fsWalkFunc := func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var name string
|
||||
name, err = filepath.Rel(fname, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path = filepath.Join(linkName, name)
|
||||
|
||||
// 改了这里
|
||||
if strings.Contains(path, "/leanote/public") ||
|
||||
strings.Contains(path, "/leanote/files") ||
|
||||
strings.Contains(path, "/leanote/doc") ||
|
||||
strings.Contains(path, "/leanote/logs") ||
|
||||
strings.Contains(path, "/leanote/build") ||
|
||||
strings.Contains(path, "/leanote/target") {
|
||||
s.log.Warn("public 或 files 不要处理", "path", path)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if err == nil && info.Mode() & os.ModeSymlink == os.ModeSymlink {
|
||||
var symlinkPath string
|
||||
symlinkPath, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// https://github.com/golang/go/blob/master/src/path/filepath/path.go#L392
|
||||
info, err = os.Lstat(symlinkPath)
|
||||
|
||||
if err != nil {
|
||||
return walkFn(path, info, err)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return s.fsWalk(symlinkPath, path, walkFn)
|
||||
}
|
||||
}
|
||||
|
||||
return walkFn(path, info, err)
|
||||
}
|
||||
err := filepath.Walk(fname, fsWalkFunc)
|
||||
return err
|
||||
}
|
||||
|
||||
// Using the packages.Load function load all the packages and type specifications (forces compile).
|
||||
// this sets the SourceProcessor.packageList []*packages.Package
|
||||
func (s *SourceProcessor) addPackages() (err error) {
|
||||
allPackages := []string{model.RevelImportPath + "/..."}
|
||||
for _, module := range s.revelContainer.ModulePathMap {
|
||||
allPackages = append(allPackages, module.ImportPath + "/...") // +"/app/controllers/...")
|
||||
}
|
||||
s.log.Info("Reading packages", "packageList", allPackages)
|
||||
//allPackages = []string{s.revelContainer.ImportPath + "/..."} //+"/app/controllers/..."}
|
||||
|
||||
config := &packages.Config{
|
||||
// ode: packages.NeedSyntax | packages.NeedCompiledGoFiles,
|
||||
Mode:
|
||||
packages.NeedTypes | // For compile error
|
||||
packages.NeedDeps | // To load dependent files
|
||||
packages.NeedName | // Loads the full package name
|
||||
packages.NeedSyntax, // To load ast tree (for end points)
|
||||
//Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles |
|
||||
// packages.NeedImports | packages.NeedDeps | packages.NeedExportsFile |
|
||||
// packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo |
|
||||
// packages.NeedTypesSizes,
|
||||
|
||||
//Mode: packages.NeedName | packages.NeedImports | packages.NeedDeps | packages.NeedExportsFile | packages.NeedFiles |
|
||||
// packages.NeedCompiledGoFiles | packages.NeedTypesSizes |
|
||||
// packages.NeedSyntax | packages.NeedCompiledGoFiles ,
|
||||
//Mode: packages.NeedSyntax | packages.NeedCompiledGoFiles | packages.NeedName | packages.NeedFiles |
|
||||
// packages.LoadTypes | packages.NeedTypes | packages.NeedDeps, //, // |
|
||||
// packages.NeedTypes, // packages.LoadTypes | packages.NeedSyntax | packages.NeedTypesInfo,
|
||||
//packages.LoadSyntax | packages.NeedDeps,
|
||||
Dir:s.revelContainer.AppPath,
|
||||
}
|
||||
s.packageList, err = packages.Load(config, allPackages...)
|
||||
s.log.Info("Loaded modules ", "len results", len(s.packageList), "error", err)
|
||||
|
||||
// Now process the files in the aap source folder s.revelContainer.ImportPath + "/...",
|
||||
err = s.Walk(s.revelContainer.BasePath, s.processPath)
|
||||
s.log.Info("Loaded apps and modules ", "len results", len(s.packageList), "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// This callback is used to build the packages for the "app" package. This allows us to
|
||||
// parse the source files without doing a full compile on them
|
||||
// This callback only processes folders, so any files passed to this will return a nil
|
||||
func (s *SourceProcessor) processPath(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
s.log.Error("Error scanning app source:", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ignore files and folders not marked tmp (since those are generated)
|
||||
if !info.IsDir() || info.Name() == "tmp" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Real work for processing the folder
|
||||
pkgImportPath := s.revelContainer.ImportPath
|
||||
appPath := s.revelContainer.BasePath
|
||||
if appPath != path {
|
||||
pkgImportPath = s.revelContainer.ImportPath + "/" + filepath.ToSlash(path[len(appPath) + 1:])
|
||||
}
|
||||
s.log.Info("Processing source package folder", "package", pkgImportPath, "path", path)
|
||||
|
||||
// Parse files within the path.
|
||||
var pkgMap map[string]*ast.Package
|
||||
fset := token.NewFileSet()
|
||||
pkgMap, err = parser.ParseDir(
|
||||
fset,
|
||||
path,
|
||||
func(f os.FileInfo) bool {
|
||||
return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go")
|
||||
},
|
||||
0)
|
||||
|
||||
if err != nil {
|
||||
if errList, ok := err.(scanner.ErrorList); ok {
|
||||
var pos = errList[0].Pos
|
||||
newError := &utils.SourceError{
|
||||
SourceType: ".go source",
|
||||
Title: "Go Compilation Error",
|
||||
Path: pos.Filename,
|
||||
Description: errList[0].Msg,
|
||||
Line: pos.Line,
|
||||
Column: pos.Column,
|
||||
SourceLines: utils.MustReadLines(pos.Filename),
|
||||
}
|
||||
|
||||
errorLink := s.revelContainer.Config.StringDefault("error.link", "")
|
||||
if errorLink != "" {
|
||||
newError.SetLink(errorLink)
|
||||
}
|
||||
return newError
|
||||
}
|
||||
|
||||
// This is exception, err already checked above. Here just a print
|
||||
ast.Print(nil, err)
|
||||
s.log.Fatal("Failed to parse dir", "error", err)
|
||||
}
|
||||
|
||||
// Skip "main" packages.
|
||||
delete(pkgMap, "main")
|
||||
|
||||
// Ignore packages that end with _test
|
||||
// These cannot be included in source code that is not generated specifically as a test
|
||||
for i := range pkgMap {
|
||||
if len(i) > 6 {
|
||||
if string(i[len(i) - 5:]) == "_test" {
|
||||
delete(pkgMap, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no code in this directory, skip it.
|
||||
if len(pkgMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// There should be only one package in this directory.
|
||||
if len(pkgMap) > 1 {
|
||||
for i := range pkgMap {
|
||||
println("Found duplicate packages in single directory ", i)
|
||||
}
|
||||
utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgMap)
|
||||
}
|
||||
|
||||
// At this point there is only one package in the pkgs map,
|
||||
p := &packages.Package{}
|
||||
p.PkgPath = pkgImportPath
|
||||
p.Fset = fset
|
||||
for _, pkg := range pkgMap {
|
||||
p.Name = pkg.Name
|
||||
s.log.Info("Found package", "pkg.Name", pkg.Name, "p.Name", p.PkgPath)
|
||||
for filename, astFile := range pkg.Files {
|
||||
p.Syntax = append(p.Syntax, astFile)
|
||||
p.GoFiles = append(p.GoFiles, filename)
|
||||
}
|
||||
}
|
||||
s.packageList = append(s.packageList, p)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function is used to populate a map so that we can lookup controller embedded types in order to determine
|
||||
// if a Struct inherits from from revel.Controller
|
||||
func (s *SourceProcessor) addImportMap() (err error) {
|
||||
s.importMap = map[string]string{}
|
||||
s.packageMap = map[string]string{}
|
||||
for _, p := range s.packageList {
|
||||
|
||||
if len(p.Errors) > 0 {
|
||||
// Generate a compile error
|
||||
for _, e := range p.Errors {
|
||||
s.log.Info("While reading packages encountered import error ignoring ", "PkgPath", p.PkgPath, "error", e)
|
||||
}
|
||||
}
|
||||
for _, tree := range p.Syntax {
|
||||
s.importMap[tree.Name.Name] = p.PkgPath
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SourceProcessor) addSourceInfo() (err error) {
|
||||
for _, p := range s.packageList {
|
||||
if sourceInfo := s.sourceInfoProcessor.processPackage(p); sourceInfo != nil {
|
||||
if s.sourceInfo != nil {
|
||||
s.sourceInfo.Merge(sourceInfo)
|
||||
} else {
|
||||
s.sourceInfo = sourceInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
151
app/cmd/revel.go
Normal file
151
app/cmd/revel.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
|
||||
// Revel Framework source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The command line tool for running Revel apps.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
||||
"github.com/agtorre/gocolorize"
|
||||
"github.com/revel/cmd/logger"
|
||||
"github.com/revel/cmd/model"
|
||||
"github.com/revel/cmd/utils"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
const (
|
||||
// RevelCmdImportPath Revel framework cmd tool import path
|
||||
RevelCmdImportPath = "github.com/revel/cmd"
|
||||
|
||||
// RevelCmdImportPath Revel framework cmd tool import path
|
||||
RevelSkeletonsImportPath = "github.com/revel/skeletons"
|
||||
|
||||
// DefaultRunMode for revel's application
|
||||
DefaultRunMode = "dev"
|
||||
)
|
||||
|
||||
// Command structure cribbed from the genius organization of the "go" command.
|
||||
type Command struct {
|
||||
UpdateConfig func(c *model.CommandConfig, args []string) bool
|
||||
RunWith func(c *model.CommandConfig) error
|
||||
UsageLine, Short, Long string
|
||||
}
|
||||
|
||||
// Name returns command name from usage line
|
||||
func (cmd *Command) Name() string {
|
||||
name := cmd.UsageLine
|
||||
i := strings.Index(name, " ")
|
||||
if i >= 0 {
|
||||
name = name[:i]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// The commands
|
||||
var Commands = []*Command{
|
||||
nil, // Safety net, prevent missing index from running
|
||||
|
||||
// 只改了这个
|
||||
nil,
|
||||
nil,
|
||||
cmdBuild,
|
||||
}
|
||||
|
||||
func main() {
|
||||
if runtime.GOOS == "windows" {
|
||||
gocolorize.SetPlain(true)
|
||||
}
|
||||
c := &model.CommandConfig{}
|
||||
wd, _ := os.Getwd()
|
||||
|
||||
utils.InitLogger(wd, logger.LvlError)
|
||||
parser := flags.NewParser(c, flags.HelpFlag | flags.PassDoubleDash)
|
||||
if len(os.Args) < 2 {
|
||||
parser.WriteHelp(os.Stdout)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := ParseArgs(c, parser, os.Args[1:]); err != nil {
|
||||
fmt.Fprint(os.Stderr, err.Error() + "\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Switch based on the verbose flag
|
||||
if len(c.Verbose) > 1 {
|
||||
utils.InitLogger(wd, logger.LvlDebug)
|
||||
} else if len(c.Verbose) > 0 {
|
||||
utils.InitLogger(wd, logger.LvlInfo)
|
||||
} else {
|
||||
utils.InitLogger(wd, logger.LvlWarn)
|
||||
}
|
||||
|
||||
// Setup package resolver
|
||||
c.InitPackageResolver()
|
||||
|
||||
if err := c.UpdateImportPath(); err != nil {
|
||||
utils.Logger.Error(err.Error())
|
||||
parser.WriteHelp(os.Stdout)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
command := Commands[c.Index]
|
||||
println("Revel executing:", command.Short)
|
||||
|
||||
if err := command.RunWith(c); err != nil {
|
||||
utils.Logger.Error("Unable to execute", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the arguments passed into the model.CommandConfig
|
||||
func ParseArgs(c *model.CommandConfig, parser *flags.Parser, args []string) (err error) {
|
||||
var extraArgs []string
|
||||
if ini := flag.String("ini", "none", ""); *ini != "none" {
|
||||
if err = flags.NewIniParser(parser).ParseFile(*ini); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if extraArgs, err = parser.ParseArgs(args); err != nil {
|
||||
return
|
||||
} else {
|
||||
switch parser.Active.Name {
|
||||
case "new":
|
||||
c.Index = model.NEW
|
||||
case "run":
|
||||
c.Index = model.RUN
|
||||
case "build":
|
||||
c.Index = model.BUILD
|
||||
case "package":
|
||||
c.Index = model.PACKAGE
|
||||
case "clean":
|
||||
c.Index = model.CLEAN
|
||||
case "test":
|
||||
c.Index = model.TEST
|
||||
case "version":
|
||||
c.Index = model.VERSION
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !Commands[c.Index].UpdateConfig(c, extraArgs) {
|
||||
buffer := &bytes.Buffer{}
|
||||
parser.WriteHelp(buffer)
|
||||
err = fmt.Errorf("Invalid command line arguements %v\n%s", extraArgs, buffer.String())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
@@ -46,14 +46,17 @@ func (c Attach) uploadAttach(noteId string) (re info.Re) {
|
||||
return re
|
||||
}
|
||||
|
||||
file, handel, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
return re
|
||||
}
|
||||
defer file.Close()
|
||||
var data []byte
|
||||
c.Params.Bind(&data, "file")
|
||||
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
// file, handel, err := c.Request.FormFile("file")
|
||||
// if err != nil {
|
||||
// return re
|
||||
// }
|
||||
// defer file.Close()
|
||||
|
||||
// data, err := ioutil.ReadAll(file)
|
||||
if data == nil || len(data) == 0 {
|
||||
return re
|
||||
}
|
||||
// > 5M?
|
||||
@@ -71,10 +74,13 @@ func (c Attach) uploadAttach(noteId string) (re info.Re) {
|
||||
newGuid := NewGuid()
|
||||
filePath := "files/" + GetRandomFilePath(c.GetUserId(), newGuid) + "/attachs"
|
||||
dir := revel.BasePath + "/" + filePath
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return re
|
||||
}
|
||||
|
||||
handel := c.Params.Files["file"][0]
|
||||
|
||||
// 生成新的文件名
|
||||
filename := handel.Filename
|
||||
_, ext := SplitFilename(filename) // .doc
|
||||
|
||||
@@ -27,7 +27,7 @@ func (c *BaseController) Message(message string, args ...interface{}) (value str
|
||||
|
||||
func (c BaseController) GetUserId() string {
|
||||
if userId, ok := c.Session["UserId"]; ok {
|
||||
return userId
|
||||
return userId.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -47,51 +47,30 @@ func (c BaseController) GetObjectUserId() bson.ObjectId {
|
||||
|
||||
func (c BaseController) GetEmail() string {
|
||||
if email, ok := c.Session["Email"]; ok {
|
||||
return email
|
||||
return email.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c BaseController) GetUsername() string {
|
||||
if email, ok := c.Session["Username"]; ok {
|
||||
return email
|
||||
return email.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 得到用户信息
|
||||
func (c BaseController) GetUserInfo() info.User {
|
||||
if userId, ok := c.Session["UserId"]; ok && userId != "" {
|
||||
userId := c.GetUserId()
|
||||
if userId != "" {
|
||||
return userService.GetUserInfo(userId)
|
||||
/*
|
||||
notebookWidth, _ := strconv.Atoi(c.Session["NotebookWidth"])
|
||||
noteListWidth, _ := strconv.Atoi(c.Session["NoteListWidth"])
|
||||
mdEditorWidth, _ := strconv.Atoi(c.Session["MdEditorWidth"])
|
||||
LogJ(c.Session)
|
||||
user := info.User{UserId: bson.ObjectIdHex(userId),
|
||||
Email: c.Session["Email"],
|
||||
Logo: c.Session["Logo"],
|
||||
Username: c.Session["Username"],
|
||||
UsernameRaw: c.Session["UsernameRaw"],
|
||||
Theme: c.Session["Theme"],
|
||||
NotebookWidth: notebookWidth,
|
||||
NoteListWidth: noteListWidth,
|
||||
MdEditorWidth: mdEditorWidth,
|
||||
}
|
||||
if c.Session["Verified"] == "1" {
|
||||
user.Verified = true
|
||||
}
|
||||
if c.Session["LeftIsMin"] == "1" {
|
||||
user.LeftIsMin = true
|
||||
}
|
||||
return user
|
||||
*/
|
||||
}
|
||||
return info.User{}
|
||||
}
|
||||
|
||||
func (c BaseController) GetUserAndBlogUrl() info.UserAndBlogUrl {
|
||||
if userId, ok := c.Session["UserId"]; ok && userId != "" {
|
||||
userId := c.GetUserId()
|
||||
if userId != "" {
|
||||
return userService.GetUserAndBlogUrl(userId)
|
||||
}
|
||||
return info.UserAndBlogUrl{}
|
||||
@@ -103,7 +82,7 @@ func (c BaseController) GetSession(key string) string {
|
||||
if !ok {
|
||||
v = ""
|
||||
}
|
||||
return v
|
||||
return v.(string)
|
||||
}
|
||||
func (c BaseController) SetSession(userInfo info.User) {
|
||||
if userInfo.UserId.Hex() != "" {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
// "encoding/json"
|
||||
"fmt"
|
||||
"github.com/leanote/leanote/app/info"
|
||||
. "github.com/leanote/leanote/app/lea"
|
||||
// . "github.com/leanote/leanote/app/lea"
|
||||
"github.com/leanote/leanote/app/lea/blog"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
// "github.com/leanote/leanote/app/types"
|
||||
@@ -77,7 +77,7 @@ func (c Blog) e404(themePath string) revel.Result {
|
||||
// life.leanote.com
|
||||
// lealife.com
|
||||
func (c Blog) domain() (ok bool, userBlog info.UserBlog) {
|
||||
host := c.Request.Request.Host // a.cc.com:9000
|
||||
host := c.Request.Host // a.cc.com:9000
|
||||
hostArr := strings.Split(host, ".")
|
||||
if strings.Contains(host, configService.GetDefaultDomain()) {
|
||||
// 有二级域名 a.leanoe.com 3个
|
||||
@@ -110,7 +110,7 @@ func (c Blog) setPreviewUrl() {
|
||||
if username != "" {
|
||||
userIdOrEmail = username
|
||||
}
|
||||
themeId := c.Session["themeId"]
|
||||
themeId := c.GetSession("themeId")
|
||||
theme := themeService.GetTheme(userId, themeId)
|
||||
|
||||
// siteUrl := configService.GetSiteUrl()
|
||||
@@ -609,7 +609,7 @@ func (c Blog) Post(userIdOrEmail, noteId string) (re revel.Result) {
|
||||
hasDomain, userBlog := c.domain()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
Log(err)
|
||||
// Log(err)
|
||||
re = c.e404(userBlog.ThemePath)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
// "path"
|
||||
// "strconv"
|
||||
"net/http"
|
||||
"io"
|
||||
)
|
||||
|
||||
// 验证码服务
|
||||
@@ -30,9 +31,10 @@ func (r Ca) Apply(req *revel.Request, resp *revel.Response) {
|
||||
func (c Captcha) Get() revel.Result {
|
||||
c.Response.ContentType = "image/png"
|
||||
image, str := captcha.Fetch()
|
||||
image.WriteTo(c.Response.Out)
|
||||
out := io.Writer(c.Response.GetWriter())
|
||||
image.WriteTo(out)
|
||||
|
||||
sessionId := c.Session["_ID"]
|
||||
sessionId := c.GetSession("_ID")
|
||||
// LogJ(c.Session)
|
||||
// Log("------")
|
||||
// Log(str)
|
||||
|
||||
@@ -97,11 +97,27 @@ func (c File) uploadImage(from, albumId string) (re info.Re) {
|
||||
re.Ok = Ok
|
||||
}()
|
||||
|
||||
file, handel, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
// file, handel, err := c.Request.FormFile("file")
|
||||
// if err != nil {
|
||||
// return re
|
||||
// }
|
||||
// defer file.Close()
|
||||
|
||||
var data []byte
|
||||
c.Params.Bind(&data, "file")
|
||||
handel := c.Params.Files["file"][0]
|
||||
if data == nil || len(data) == 0 {
|
||||
return re
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// file, handel, err := c.Request.FormFile("file")
|
||||
// if err != nil {
|
||||
// return re
|
||||
// }
|
||||
// defer file.Close()
|
||||
|
||||
// data, err := ioutil.ReadAll(file)
|
||||
|
||||
|
||||
// 生成上传路径
|
||||
newGuid := NewGuid()
|
||||
@@ -116,7 +132,7 @@ func (c File) uploadImage(from, albumId string) (re info.Re) {
|
||||
}
|
||||
|
||||
dir := revel.BasePath + "/" + fileUrlPath
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return re
|
||||
}
|
||||
@@ -136,11 +152,11 @@ func (c File) uploadImage(from, albumId string) (re info.Re) {
|
||||
}
|
||||
|
||||
filename = newGuid + ext
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
LogJ(err)
|
||||
return re
|
||||
}
|
||||
// data, err := ioutil.ReadAll(file)
|
||||
// if err != nil {
|
||||
// LogJ(err)
|
||||
// return re
|
||||
// }
|
||||
|
||||
var maxFileSize float64
|
||||
if from == "logo" {
|
||||
|
||||
@@ -234,20 +234,21 @@ func (c Note) UpdateNoteOrContent(noteOrContent info.NoteOrContent) revel.Result
|
||||
}
|
||||
|
||||
//-------------
|
||||
afterContentUsn := 0
|
||||
contentOk := false
|
||||
contentMsg := ""
|
||||
// afterContentUsn := 0
|
||||
// contentOk := false
|
||||
// contentMsg := ""
|
||||
if c.Has("Content") {
|
||||
// noteService.UpdateNoteContent(noteOrContent.UserId, c.GetUserId(),
|
||||
// noteOrContent.NoteId, noteOrContent.Content, noteOrContent.Abstract)
|
||||
contentOk, contentMsg, afterContentUsn = noteService.UpdateNoteContent(c.GetUserId(),
|
||||
// contentOk, contentMsg, afterContentUsn =
|
||||
noteService.UpdateNoteContent(c.GetUserId(),
|
||||
noteOrContent.NoteId, noteOrContent.Content, noteOrContent.Abstract,
|
||||
needUpdateNote, -1, time.Now())
|
||||
}
|
||||
|
||||
Log(afterContentUsn)
|
||||
Log(contentOk)
|
||||
Log(contentMsg)
|
||||
// Log("usn", "afterContentUsn", afterContentUsn + "")
|
||||
// Log(contentOk)
|
||||
// Log(contentMsg)
|
||||
|
||||
return c.RenderJSON(true)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func (c Preview) getPreviewThemeAbsolutePath(themeId string) bool {
|
||||
if themeId != "" {
|
||||
c.Session["themeId"] = themeId // 存到session中, 下次的url就不能带了, 待优化, 有时会取不到
|
||||
} else {
|
||||
themeId = c.Session["themeId"] // 直接从session中获取
|
||||
themeId = c.GetSession("themeId") // 直接从session中获取
|
||||
}
|
||||
if themeId == "" {
|
||||
return false
|
||||
|
||||
@@ -83,7 +83,7 @@ func AuthInterceptor(c *revel.Controller) revel.Result {
|
||||
|
||||
// 验证是否已登录
|
||||
// 必须是管理员
|
||||
if username, ok := c.Session["Username"]; ok && username == configService.GetAdminUsername() {
|
||||
if username, ok := c.Session["Username"]; ok && username.(string) == configService.GetAdminUsername() {
|
||||
return nil // 已登录
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,18 @@ type ApiBaseContrller struct {
|
||||
|
||||
// 得到token, 这个token是在AuthInterceptor设到Session中的
|
||||
func (c ApiBaseContrller) getToken() string {
|
||||
return c.Session["_token"]
|
||||
return c.GetSession("_token")
|
||||
}
|
||||
|
||||
// userId
|
||||
// _userId是在AuthInterceptor设置的
|
||||
func (c ApiBaseContrller) getUserId() string {
|
||||
return c.Session["_userId"]
|
||||
return c.GetSession("_userId")
|
||||
}
|
||||
|
||||
// 得到用户信息
|
||||
func (c ApiBaseContrller) getUserInfo() info.User {
|
||||
userId := c.Session["_userId"]
|
||||
userId := c.GetSession("_userId")
|
||||
if userId == "" {
|
||||
return info.User{}
|
||||
}
|
||||
@@ -53,16 +53,23 @@ func (c ApiBaseContrller) uploadAttach(name string, noteId string) (ok bool, msg
|
||||
}
|
||||
*/
|
||||
|
||||
file, handel, err := c.Request.FormFile(name)
|
||||
if err != nil {
|
||||
var data []byte
|
||||
c.Params.Bind(&data, name)
|
||||
handel := c.Params.Files[name][0]
|
||||
if data == nil || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// file, handel, err := c.Request.FormFile(name)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// defer file.Close()
|
||||
|
||||
// data, err := ioutil.ReadAll(file)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// > 5M?
|
||||
maxFileSize := configService.GetUploadSize("uploadAttachSize")
|
||||
if maxFileSize <= 0 {
|
||||
@@ -79,7 +86,7 @@ func (c ApiBaseContrller) uploadAttach(name string, noteId string) (ok bool, msg
|
||||
filePath := "files/" + GetRandomFilePath(userId, newGuid) + "/attachs"
|
||||
|
||||
dir := revel.BasePath + "/" + filePath
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -122,11 +129,18 @@ func (c ApiBaseContrller) upload(name string, noteId string, isAttach bool) (ok
|
||||
if isAttach {
|
||||
return c.uploadAttach(name, noteId)
|
||||
}
|
||||
file, handel, err := c.Request.FormFile(name)
|
||||
if err != nil {
|
||||
// file, handel, err := c.Request.FormFile(name)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// defer file.Close()
|
||||
|
||||
var data []byte
|
||||
c.Params.Bind(&data, name)
|
||||
handel := c.Params.Files[name][0]
|
||||
if data == nil || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
newGuid := NewGuid()
|
||||
// 生成上传路径
|
||||
@@ -135,7 +149,7 @@ func (c ApiBaseContrller) upload(name string, noteId string, isAttach bool) (ok
|
||||
fileUrlPath := "files/" + GetRandomFilePath(userId, newGuid) + "/images"
|
||||
|
||||
dir := revel.BasePath + "/" + fileUrlPath
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -148,10 +162,10 @@ func (c ApiBaseContrller) upload(name string, noteId string, isAttach bool) (ok
|
||||
// }
|
||||
|
||||
filename = newGuid + ext
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// data, err := ioutil.ReadAll(file)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
|
||||
maxFileSize := configService.GetUploadSize("uploadImageSize")
|
||||
if maxFileSize <= 0 {
|
||||
|
||||
@@ -178,7 +178,7 @@ func (c ApiNote) fixPostNotecontent(noteOrContent *info.ApiNote) {
|
||||
// "/api/file/getImage?fileId="+file.FileId, -1)
|
||||
} else {
|
||||
reg, _ := regexp.Compile(`https*://[^/]*?/api/file/getAttach\?fileId=`+file.LocalFileId)
|
||||
Log(reg)
|
||||
// Log(reg)
|
||||
noteOrContent.Content = reg.ReplaceAllString(noteOrContent.Content, `/api/file/getAttach?fileId=`+file.FileId)
|
||||
/*
|
||||
noteOrContent.Content = strings.Replace(noteOrContent.Content,
|
||||
|
||||
@@ -104,16 +104,23 @@ func (c ApiUser) uploadImage() (ok bool, msg, url string) {
|
||||
var fileUrlPath = ""
|
||||
ok = false
|
||||
|
||||
file, handel, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
var data []byte
|
||||
c.Params.Bind(&data, "file")
|
||||
handel := c.Params.Files["file"][0]
|
||||
if data == nil || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// file, handel, err := c.Request.FormFile("file")
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// defer file.Close()
|
||||
// 生成上传路径
|
||||
fileUrlPath = "public/upload/" + c.getUserId() + "/images/logo"
|
||||
|
||||
dir := revel.BasePath + "/" + fileUrlPath
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -129,11 +136,11 @@ func (c ApiUser) uploadImage() (ok bool, msg, url string) {
|
||||
}
|
||||
|
||||
filename = NewGuid() + ext
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
LogJ(err)
|
||||
return
|
||||
}
|
||||
// data, err := ioutil.ReadAll(file)
|
||||
// if err != nil {
|
||||
// LogJ(err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// > 5M?
|
||||
if len(data) > 5*1024*1024 {
|
||||
|
||||
@@ -90,7 +90,10 @@ func AuthInterceptor(c *revel.Controller) revel.Result {
|
||||
if noToken && userId == "" {
|
||||
// 从session中获取, api/file/getImage, api/file/getAttach, api/file/getAllAttach
|
||||
// 客户端
|
||||
userId, _ = c.Session["UserId"]
|
||||
userIdI, _ := c.Session["UserId"]
|
||||
if userIdI != nil {
|
||||
userId = userIdI.(string)
|
||||
}
|
||||
}
|
||||
c.Session["_userId"] = userId
|
||||
|
||||
|
||||
@@ -351,14 +351,21 @@ func (c MemberBlog) uploadImage(themeId string) (re info.Re) {
|
||||
re.Ok = Ok
|
||||
}()
|
||||
|
||||
file, handel, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
var data []byte
|
||||
c.Params.Bind(&data, "file")
|
||||
handel := c.Params.Files["file"][0]
|
||||
if data == nil || len(data) == 0 {
|
||||
return re
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// file, handel, err := c.Request.FormFile("file")
|
||||
// if err != nil {
|
||||
// return re
|
||||
// }
|
||||
// defer file.Close()
|
||||
// 生成上传路径
|
||||
dir := themeService.GetThemeAbsolutePath(c.GetUserId(), themeId) + "/images"
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return re
|
||||
}
|
||||
@@ -374,11 +381,11 @@ func (c MemberBlog) uploadImage(themeId string) (re info.Re) {
|
||||
}
|
||||
|
||||
filename = filename
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
LogJ(err)
|
||||
return re
|
||||
}
|
||||
// data, err := ioutil.ReadAll(file)
|
||||
// if err != nil {
|
||||
// LogJ(err)
|
||||
// return re
|
||||
// }
|
||||
|
||||
// > 2M?
|
||||
if len(data) > 5*1024*1024 {
|
||||
@@ -441,17 +448,24 @@ func (c MemberBlog) ExportTheme(themeId string) revel.Result {
|
||||
func (c MemberBlog) ImportTheme() revel.Result {
|
||||
re := info.NewRe()
|
||||
|
||||
file, handel, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
re.Msg = fmt.Sprintf("%v", err)
|
||||
var data []byte
|
||||
c.Params.Bind(&data, "file")
|
||||
handel := c.Params.Files["file"][0]
|
||||
if data == nil || len(data) == 0 {
|
||||
return c.RenderJSON(re)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
// file, handel, err := c.Request.FormFile("file")
|
||||
// if err != nil {
|
||||
// re.Msg = fmt.Sprintf("%v", err)
|
||||
// return c.RenderJSON(re)
|
||||
// }
|
||||
|
||||
// defer file.Close()
|
||||
// 生成上传路径
|
||||
userId := c.GetUserId()
|
||||
dir := revel.BasePath + "/public/upload/" + userId + "/tmp"
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
re.Msg = fmt.Sprintf("%v", err)
|
||||
return c.RenderJSON(re)
|
||||
@@ -467,10 +481,10 @@ func (c MemberBlog) ImportTheme() revel.Result {
|
||||
}
|
||||
|
||||
filename = filename
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return c.RenderJSON(re)
|
||||
}
|
||||
// data, err := ioutil.ReadAll(file)
|
||||
// if err != nil {
|
||||
// return c.RenderJSON(re)
|
||||
// }
|
||||
|
||||
// > 10M?
|
||||
if len(data) > 10*1024*1024 {
|
||||
|
||||
@@ -76,6 +76,11 @@ func Init(url, dbname string) {
|
||||
// get dbname from urlEnv
|
||||
urls := strings.Split(url, "/")
|
||||
dbname = urls[len(urls)-1]
|
||||
|
||||
if strings.Contains(dbname, "?") {
|
||||
urls = strings.Split(dbname, "?")
|
||||
dbname = urls[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
if dbname == "" {
|
||||
|
||||
@@ -6,21 +6,21 @@ import (
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func Log(i ...interface{}) {
|
||||
revel.INFO.Println(i...)
|
||||
func Log(msg string, i ...interface{}) {
|
||||
revel.AppLog.Info(msg, i...)
|
||||
}
|
||||
|
||||
func LogW(i ...interface{}) {
|
||||
revel.WARN.Println(i...)
|
||||
func Logf(msg string, i ...interface{}) {
|
||||
revel.AppLog.Infof(msg, i...)
|
||||
}
|
||||
|
||||
func Log1(key, i interface{}) {
|
||||
revel.INFO.Println(key, i)
|
||||
func LogW(msg string, i ...interface{}) {
|
||||
revel.AppLog.Warn(msg, i...)
|
||||
}
|
||||
|
||||
func LogJ(i interface{}) {
|
||||
b, _ := json.MarshalIndent(i, "", " ")
|
||||
revel.INFO.Println(string(b))
|
||||
revel.AppLog.Info(string(b))
|
||||
}
|
||||
|
||||
// 为test用
|
||||
|
||||
@@ -79,7 +79,6 @@ func SendEmailOld(to, subject, body string) bool {
|
||||
err := smtp.SendMail(host+":"+port, auth, username, send_to, msg)
|
||||
|
||||
if err != nil {
|
||||
Log(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -183,7 +183,6 @@ func PutFileStrContent(path, content string) bool {
|
||||
// Log(path)
|
||||
|
||||
if err1 != nil {
|
||||
Log(err1)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -400,9 +400,6 @@ func FixFilename(filename string) string {
|
||||
func IsValidTime(t time.Time) bool {
|
||||
if t.Year() > 20 {
|
||||
now := time.Now()
|
||||
Log("------")
|
||||
Log(t)
|
||||
Log(now)
|
||||
if t.Before(now) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func (r *RenderTemplateResult) Apply(req *revel.Request, resp *revel.Response) {
|
||||
chunked := revel.Config.BoolDefault("results.chunked", false)
|
||||
|
||||
// If it's a HEAD request, throw away the bytes.
|
||||
out := io.Writer(resp.Out)
|
||||
out := io.Writer(resp.GetWriter())
|
||||
if req.Method == "HEAD" {
|
||||
out = ioutil.Discard
|
||||
}
|
||||
@@ -289,7 +289,7 @@ func (r ErrorResult) Apply(req *revel.Request, resp *revel.Response) {
|
||||
// 不是preview就不要显示错误了
|
||||
if r.IsPreview {
|
||||
var b bytes.Buffer
|
||||
out := io.Writer(resp.Out)
|
||||
out := io.Writer(resp.GetWriter())
|
||||
// out = ioutil.Discard
|
||||
err = tmpl.Execute(&b, r.ViewArgs)
|
||||
resp.WriteHeader(http.StatusOK, "text/html; charset=utf-8")
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
. "github.com/leanote/leanote/app/lea"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -146,15 +147,15 @@ func loadMessageFile(locale string, path string, info os.FileInfo, osError error
|
||||
// If we have already parsed a message file for this locale, merge both
|
||||
if _, exists := messages[locale]; exists {
|
||||
messages[locale].Merge(config)
|
||||
revel.TRACE.Printf("Successfully merged messages for locale '%s'", locale)
|
||||
Logf("Successfully merged messages for locale '%s'", locale)
|
||||
} else {
|
||||
messages[locale] = config
|
||||
}
|
||||
|
||||
revel.TRACE.Println("Successfully loaded messages from file", info.Name())
|
||||
Logf("Successfully loaded messages from file", info.Name())
|
||||
}
|
||||
} else {
|
||||
revel.TRACE.Printf("Ignoring file %s because it did not have a valid extension", info.Name())
|
||||
Logf("Ignoring file %s because it did not have a valid extension", info.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -178,13 +179,13 @@ func init() {
|
||||
|
||||
func I18nFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
if foundCookie, cookieValue := hasLocaleCookie(c.Request); foundCookie {
|
||||
revel.TRACE.Printf("Found locale cookie value: %s", cookieValue)
|
||||
// revel.TRACE.Printf("Found locale cookie value: %s", cookieValue)
|
||||
setCurrentLocaleControllerArguments(c, cookieValue)
|
||||
} else if foundHeader, headerValue := hasAcceptLanguageHeader(c.Request); foundHeader {
|
||||
revel.TRACE.Printf("Found Accept-Language header value: %s", headerValue)
|
||||
// revel.TRACE.Printf("Found Accept-Language header value: %s", headerValue)
|
||||
setCurrentLocaleControllerArguments(c, headerValue)
|
||||
} else {
|
||||
revel.TRACE.Println("Unable to find locale in cookie or header, using empty string")
|
||||
// revel.TRACE.Println("Unable to find locale in cookie or header, using empty string")
|
||||
setCurrentLocaleControllerArguments(c, "")
|
||||
}
|
||||
fc[0](c, fc[1:])
|
||||
@@ -210,12 +211,12 @@ func hasAcceptLanguageHeader(request *revel.Request) (bool, string) {
|
||||
|
||||
// Determine whether the given request has a valid language cookie value.
|
||||
func hasLocaleCookie(request *revel.Request) (bool, string) {
|
||||
if request != nil && request.Cookies() != nil {
|
||||
if request != nil {
|
||||
name := revel.Config.StringDefault(localeCookieConfigKey, revel.CookiePrefix+"_LANG")
|
||||
if cookie, error := request.Cookie(name); error == nil {
|
||||
return true, cookie.Value
|
||||
return true, cookie.GetValue()
|
||||
} else {
|
||||
revel.TRACE.Printf("Unable to read locale cookie with name '%s': %s", name, error.Error())
|
||||
// revel.TRACE.Printf("Unable to read locale cookie with name '%s': %s", name, error.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ func WriteUrl(url string, toPath string) (length int64, newFilename, path string
|
||||
func GetContent(url string) (content []byte, err error) {
|
||||
var resp *http.Response
|
||||
resp, err = http.Get(url)
|
||||
Log(err)
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
} else {
|
||||
@@ -65,7 +64,6 @@ func GetContent(url string) (content []byte, err error) {
|
||||
var buf []byte
|
||||
buf, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
Log(err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package route
|
||||
import (
|
||||
"github.com/leanote/leanote/app/db"
|
||||
"github.com/revel/revel"
|
||||
// . "github.com/leanote/leanote/app/lea"
|
||||
// . "github.com/leanote/leanote/app/lea"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
@@ -14,15 +14,19 @@ var staticPrefix = []string{"/public", "/favicon.ico", "/css", "/js", "/images",
|
||||
|
||||
func RouterFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
// 补全controller部分
|
||||
path := c.Request.Request.URL.Path
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Figure out the Controller/Action
|
||||
var route *revel.RouteMatch = revel.MainRouter.Route(c.Request.Request)
|
||||
// var route *revel.RouteMatch = revel.MainRouter.Route(c.Request.Request)
|
||||
route := revel.MainRouter.Route(c.Request)
|
||||
|
||||
if route == nil {
|
||||
c.Result = c.NotFound("No matching route found: " + c.Request.RequestURI)
|
||||
c.Result = c.NotFound("No matching route found: " + c.Request.GetRequestURI())
|
||||
return
|
||||
}
|
||||
|
||||
// Log("---------" + route.Action + " " + path)
|
||||
|
||||
// The route may want to explicitly return a 404.
|
||||
if route.Action == "404" {
|
||||
c.Result = c.NotFound("(intentionally)")
|
||||
@@ -47,14 +51,23 @@ func RouterFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
// 检查mongodb 是否lost
|
||||
db.CheckMongoSessionLost()
|
||||
|
||||
// /api/file/getImage -> App\file (/api/file/getImage)
|
||||
// App\auth
|
||||
// App\note
|
||||
// static\static
|
||||
//
|
||||
// Log("---------" + route.ControllerName + " " + path)
|
||||
|
||||
// api设置
|
||||
// leanote.com/api/user/get => ApiUser::Get
|
||||
//* /api/login ApiAuth.Login, 这里的设置, 其实已经转成了ApiAuth了
|
||||
if strings.HasPrefix(path, "/api") && !strings.HasPrefix(route.ControllerName, "Api") {
|
||||
route.ControllerName = "Api" + route.ControllerName
|
||||
} else if strings.HasPrefix(path, "/member") && !strings.HasPrefix(route.ControllerName, "Member") {
|
||||
if strings.HasPrefix(path, "/api") && !strings.HasPrefix(route.ControllerName, "App\\api") {
|
||||
route.ControllerName = "App\\api" + strings.Split(route.ControllerName, "\\")[1]
|
||||
// route.ControllerName = "App\\apifile"
|
||||
} else if strings.HasPrefix(path, "/member") && !strings.HasPrefix(route.ControllerName, "App\\member") {
|
||||
// member设置
|
||||
route.ControllerName = "Member" + route.ControllerName
|
||||
// route.ControllerName = "App\\Member" + route.ControllerName
|
||||
route.ControllerName = "App\\member" + strings.Split(route.ControllerName, "\\")[1]
|
||||
}
|
||||
// end
|
||||
}
|
||||
@@ -78,7 +91,7 @@ func RouterFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
arg := c.MethodType.Args[i]
|
||||
c.Params.Fixed.Set(arg.Name, value)
|
||||
} else {
|
||||
revel.WARN.Println("Too many parameters to", route.Action, "trying to add", value)
|
||||
// revel.WARN.Println("Too many parameters to", route.Action, "trying to add", value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Compress and combine js files
|
||||
@@ -1,159 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
// "time"
|
||||
)
|
||||
|
||||
/*
|
||||
用golang exec 总是说找不到uglifyjs命令, 需要全部路径
|
||||
而且node, npm要在/usr/bin下, 已建ln
|
||||
*/
|
||||
|
||||
/*
|
||||
<script src="js/jquery-1.9.0.min.js"></script>
|
||||
|
||||
<!-- 以后将所有的js压缩合并成一个文件 -->
|
||||
<script src="js/jquery-cookie.js"></script>
|
||||
<script src="js/bootstrap.js"></script>
|
||||
<script type="text/javascript" src="tinymce/tinymce.js"></script>
|
||||
<script src="js/common.js"></script>
|
||||
<script src="js/app/note.js"></script>
|
||||
<script src="js/app/tag.js"></script>
|
||||
<script src="js/app/notebook.js"></script>
|
||||
<script src="js/app/share.js"></script>
|
||||
<script src="js/object_id.js"></script>
|
||||
<script type="text/javascript" src="js/ZeroClipboard/ZeroClipboard.js"></script>
|
||||
*/
|
||||
|
||||
//var jss = []string{"js/jquery-cookie", "js/bootstrap"}
|
||||
var jss = []string{"js/jquery-cookie", "js/bootstrap",
|
||||
"js/common", "js/app/note", "js/app/tag", "js/app/notebook", "js/app/share",
|
||||
"js/object_id", "js/ZeroClipboard/ZeroClipboard"}
|
||||
|
||||
var base1 = "/Users/life/Documents/Go/package2/src/github.com/leanote/leanote/"
|
||||
var base = "/Users/life/Documents/Go/package2/src/github.com/leanote/leanote/public/"
|
||||
var cmdPath = "/usr/local/bin/uglifyjs"
|
||||
|
||||
func cmdError(err error) {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Fprintf(os.Stderr, "The command failed to perform: %s (Command: %s, Arguments: %s)", err, "", "")
|
||||
} else {
|
||||
fmt.Println("OK")
|
||||
}
|
||||
}
|
||||
|
||||
// filename没有扩展名
|
||||
func compressJs(filename string) {
|
||||
source := base + filename + ".js"
|
||||
to := base + filename + "-min.js"
|
||||
cmd := exec.Command(cmdPath, source, "-o", to)
|
||||
_, err := cmd.CombinedOutput()
|
||||
fmt.Println(source)
|
||||
cmdError(err)
|
||||
}
|
||||
|
||||
func combineJs() {
|
||||
// 生成一个总文件
|
||||
cmd := exec.Command("rm", base+"js/all.js")
|
||||
_, err := cmd.CombinedOutput()
|
||||
cmdError(err)
|
||||
|
||||
for _, js := range jss {
|
||||
to := base + js + "-min.js"
|
||||
fmt.Println(to)
|
||||
compressJs(js)
|
||||
|
||||
// 每个压缩后的文件放入之
|
||||
cmd2 := exec.Command("/bin/sh", "-c", "cat "+to+" >> "+base+"js/all.js")
|
||||
_, err := cmd2.CombinedOutput()
|
||||
cmdError(err)
|
||||
cmd2 = exec.Command("/bin/sh", "-c", "cat \n >> "+base+"js/all.js")
|
||||
_, err = cmd2.CombinedOutput()
|
||||
cmdError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 改note-dev->note
|
||||
func dev() {
|
||||
// 即替换note.js->note-min.js
|
||||
m := map[string]string{"tinymce.dev.js": "tinymce.min.js",
|
||||
"tinymce.js": "tinymce.min.js",
|
||||
"jquery.ztree.all-3.5.js": "jquery.ztree.all-3.5-min.js",
|
||||
"note.js": "note-min.js",
|
||||
"app.js": "app-min.js",
|
||||
"page.js": "page-min.js",
|
||||
"common.js": "common-min.js",
|
||||
"notebook.js": "notebook-min.js",
|
||||
"share.js": "share-min.js",
|
||||
"tag.js": "tag-min.js",
|
||||
"jquery.slimscroll.js": "jquery.slimscroll-min.js",
|
||||
"jquery.contextmenu.js": "jquery.contextmenu-min.js",
|
||||
"editor/editor.js": "editor/editor-min.js",
|
||||
"/public/mdeditor/editor/scrollLink.js": "/public/mdeditor/editor/scrollLink-min.js",
|
||||
"console.log(o);": "",
|
||||
}
|
||||
path := base1 + "/src/views/note/note-dev.html"
|
||||
target := base1 + "/src/views/note/note.html"
|
||||
|
||||
bs, _ := ioutil.ReadFile(path)
|
||||
content := string(bs)
|
||||
print(content)
|
||||
for key, value := range m {
|
||||
content = strings.Replace(content, key, value, -1)
|
||||
}
|
||||
|
||||
// var time = time.Now().Unix() % 1000
|
||||
|
||||
// content = strings.Replace(content, "-min.js", fmt.Sprintf("-min.js?r=%d", time), -1)
|
||||
// content = strings.Replace(content, "default{{end}}.css", fmt.Sprintf("default{{end}}.css?r=%d", time), 1)
|
||||
// content = strings.Replace(content, "writting-overwrite.css", fmt.Sprintf("writting-overwrite.css?r=%d", time), 1)
|
||||
|
||||
ioutil.WriteFile(target, []byte(content), os.ModeAppend)
|
||||
}
|
||||
|
||||
// 压缩js成一块
|
||||
func tinymce() {
|
||||
// cmdStr := "node_modules/jake/bin/cli.js minify bundle[themes:modern,plugins:table,paste,advlist,autolink,link,image,lists,charmap,hr,searchreplace,visualblocks,visualchars,code,nav,tabfocus,contextmenu,directionality,codemirror,codesyntax,textcolor,fullpage]"
|
||||
// cmd := exec.Command("/Users/life/Documents/eclipse-workspace/go/leanote_release/tinymce-master/node_modules/jake/bin/cli.js", "minify", "bundle[themes:modern,plugins:table,paste,advlist,autolink,link,image,lists,charmap,hr,searchreplace,visualblocks,visualchars,code,nav,tabfocus,contextmenu,directionality,codemirror,codesyntax,textcolor,fullpage]")
|
||||
cmd := exec.Command("/bin/sh", "-c", "grunt minify")
|
||||
cmd.Dir = base + "/tinymce_4.1.9"
|
||||
|
||||
fmt.Println("正在build tinymce")
|
||||
|
||||
// 必须要先删除
|
||||
cmd2 := exec.Command("/bin/sh", "-c", "rm "+cmd.Dir+"/js/tinymce/tinymce.dev.js")
|
||||
cmd2.CombinedOutput()
|
||||
cmd2 = exec.Command("/bin/sh", "-c", "rm "+cmd.Dir+"/js/tinymce/tinymce.jquery.dev.js")
|
||||
c, _ := cmd2.CombinedOutput()
|
||||
fmt.Println(string(c))
|
||||
|
||||
c, _ = cmd.CombinedOutput()
|
||||
fmt.Println(string(c))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 压缩tinymce
|
||||
// tinymce()
|
||||
|
||||
dev()
|
||||
|
||||
// 其它零散的需要压缩的js
|
||||
otherJss := []string{"js/main", "js/app/page", "js/contextmenu/jquery.contextmenu",
|
||||
"js/jquery.ztree.all-3.5",
|
||||
"js/jQuery-slimScroll-1.3.0/jquery.slimscroll",
|
||||
}
|
||||
|
||||
for _, js := range otherJss {
|
||||
compressJs(js)
|
||||
}
|
||||
|
||||
// 先压缩后合并
|
||||
combineJs()
|
||||
|
||||
}
|
||||
@@ -606,5 +606,5 @@ func (this *ConfigService) HomePageIsAdminsBlog() bool {
|
||||
}
|
||||
|
||||
func (this *ConfigService) GetVersion() string {
|
||||
return "2.5"
|
||||
return "2.6.1"
|
||||
}
|
||||
|
||||
@@ -616,7 +616,7 @@ func (this *NoteService) UpdateNoteContent(updatedUserId, noteId, content, abstr
|
||||
return false, "conflict", 0
|
||||
}
|
||||
afterUsn = userService.IncrUsn(userId)
|
||||
db.UpdateByIdAndUserIdField(db.Notes, noteId, userId, "Usn", usn)
|
||||
db.UpdateByIdAndUserIdField(db.Notes, noteId, userId, "Usn", afterUsn)
|
||||
}
|
||||
|
||||
if db.UpdateByIdAndUserIdMap(db.NoteContents, noteId, userId, data) {
|
||||
@@ -1106,7 +1106,7 @@ func (this *NoteService) FixContent(content string, isMarkdown bool) string {
|
||||
reg2, _ = regexp.Compile("<a(?:[^>]+?)(" + eachPattern["src"] + `=['"]*` + baseUrlPattern + eachPattern["middle"] + `\?` + eachPattern["param"] + `=([a-z0-9A-Z]{24})["']*)[^>]*>`)
|
||||
}
|
||||
|
||||
Log(reg2)
|
||||
// Log(reg2)
|
||||
|
||||
content = reg.ReplaceAllStringFunc(content, func(str string) string {
|
||||
// str=这样的
|
||||
|
||||
@@ -360,8 +360,8 @@ func (this *NotebookService) DragNotebooks(userId string, curNotebookId string,
|
||||
func (this *NotebookService) ReCountNotebookNumberNotes(notebookId string) bool {
|
||||
notebookIdO := bson.ObjectIdHex(notebookId)
|
||||
count := db.Count(db.Notes, bson.M{"NotebookId": notebookIdO, "IsTrash": false, "IsDeleted": false})
|
||||
Log(count)
|
||||
Log(notebookId)
|
||||
// Log(count)
|
||||
// Log(notebookId)
|
||||
return db.UpdateByQField(db.Notebooks, bson.M{"_id": notebookIdO}, "NumberNotes", count)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package service
|
||||
import (
|
||||
"github.com/leanote/leanote/app/db"
|
||||
"github.com/leanote/leanote/app/info"
|
||||
. "github.com/leanote/leanote/app/lea"
|
||||
// . "github.com/leanote/leanote/app/lea"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
"time"
|
||||
// "strings"
|
||||
@@ -66,10 +66,10 @@ func (this *SessionService) GetCaptcha(sessionId string) string {
|
||||
}
|
||||
func (this *SessionService) SetCaptcha(sessionId, captcha string) bool {
|
||||
this.Get(sessionId)
|
||||
Log(sessionId)
|
||||
Log(captcha)
|
||||
// Log(sessionId)
|
||||
// Log(captcha)
|
||||
ok := this.Update(sessionId, "Captcha", captcha)
|
||||
Log(ok)
|
||||
// Log(ok)
|
||||
return ok
|
||||
}
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ func (this *ThemeService) UpdateTplContent(userId, themeId, filename, content st
|
||||
basePath := this.GetThemeAbsolutePath(userId, themeId)
|
||||
path := basePath + "/" + filename
|
||||
if strings.Contains(filename, ".html") {
|
||||
Log(">>")
|
||||
// Log(">>")
|
||||
if ok, msg = this.ValidateTheme(basePath, filename, content); ok {
|
||||
// 模板
|
||||
if ok, msg = this.mustTpl(filename, content); ok {
|
||||
@@ -359,7 +359,7 @@ func (this *ThemeService) mustTpl(filename, content string) (ok bool, msg string
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
ok = false
|
||||
Log(err)
|
||||
// Log(err)
|
||||
msg = fmt.Sprintf("%v", err)
|
||||
}
|
||||
}()
|
||||
@@ -405,7 +405,7 @@ func (this *ThemeService) ExportTheme(userId, themeId string) (ok bool, path str
|
||||
theme := this.GetThemeById(themeId)
|
||||
// 打包
|
||||
// 验证路径, 别把整个项目打包了
|
||||
Log(theme.Path)
|
||||
// Log(theme.Path)
|
||||
if theme.Path == "" ||
|
||||
(!strings.HasPrefix(theme.Path, "public/upload") &&
|
||||
!strings.HasPrefix(theme.Path, "public/blog/themes")) ||
|
||||
@@ -417,12 +417,12 @@ func (this *ThemeService) ExportTheme(userId, themeId string) (ok bool, path str
|
||||
targetPath := revel.BasePath + "/public/upload/" + userId + "/tmp"
|
||||
err := os.MkdirAll(targetPath, 0755)
|
||||
if err != nil {
|
||||
Log(err)
|
||||
// Log(err)
|
||||
return
|
||||
}
|
||||
targetName := targetPath + "/" + theme.Name + ".zip"
|
||||
Log(sourcePath)
|
||||
Log(targetName)
|
||||
// Log(sourcePath)
|
||||
// Log(targetName)
|
||||
ok = archive.Zip(sourcePath, targetName)
|
||||
if !ok {
|
||||
return
|
||||
@@ -445,7 +445,7 @@ func (this *ThemeService) ImportTheme(userId, path string) (ok bool, msg string)
|
||||
}
|
||||
if ok, msg = archive.Unzip(path, targetPath); !ok {
|
||||
DeleteFile(targetPath)
|
||||
Log("oh no")
|
||||
// Log("oh no")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -558,8 +558,8 @@ func (this *ThemeService) InstallTheme(userId, themeId string) (ok bool) {
|
||||
// 验证主题是否全法, 存在循环引用?
|
||||
// filename, newContent 表示在修改模板时要判断模板修改时是否有错误
|
||||
func (this *ThemeService) ValidateTheme(path string, filename, newContent string) (ok bool, msg string) {
|
||||
Log("theme Path")
|
||||
Log(path)
|
||||
// Log("theme Path")
|
||||
// Log(path)
|
||||
// 建立一个有向图
|
||||
// 将该path下的所有文件提出, 得到文件的引用情况
|
||||
files := ListDir(path)
|
||||
@@ -613,9 +613,9 @@ func (this *ThemeService) ValidateTheme(path string, filename, newContent string
|
||||
for _, includes := range finds {
|
||||
include := includes[1]
|
||||
includeIndex, has := fileIndexMap[include]
|
||||
Log(includeIndex)
|
||||
Log("??")
|
||||
Log(has)
|
||||
// Log(includeIndex)
|
||||
// Log("??")
|
||||
// Log(has)
|
||||
if has {
|
||||
vector[thisIndex][includeIndex] = 1
|
||||
}
|
||||
|
||||
@@ -485,7 +485,7 @@ func (this *UserService) GetAllUserByFilter(userFilterEmail, userFilterWhiteList
|
||||
users := []info.User{}
|
||||
q := db.Users.Find(query)
|
||||
q.All(&users)
|
||||
Log(len(users))
|
||||
// Log(len(users))
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
152
bin/release.sh
152
bin/release.sh
@@ -1,152 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# release leanote
|
||||
|
||||
# 当前路径
|
||||
SP=$(cd "$(dirname "$0")"; pwd)
|
||||
|
||||
# tmp path to store leanote release files
|
||||
tmp="/Users/life/Desktop/leanote_release"
|
||||
|
||||
# version
|
||||
V="v2.5"
|
||||
|
||||
##=================================
|
||||
# 1. 先build 成 3个平台, 2种bit = 6种
|
||||
##=================================
|
||||
|
||||
# cd /Documents/Go/package2/src/github.com/leanote/leanote/bin
|
||||
# GOOS=darwin GOARCH=amd64 go build -o leanote-darwin-amd64 ../app/tmp
|
||||
|
||||
cd $SP
|
||||
|
||||
# $1 = darwin, linux
|
||||
# $2 = amd64
|
||||
function build()
|
||||
{
|
||||
echo build-$1-$2
|
||||
if [ $1 = "linux" -o $1 = "darwin" ]
|
||||
then
|
||||
suffix=""
|
||||
else
|
||||
suffix=".exe"
|
||||
fi
|
||||
|
||||
GOOS=$1 GOARCH=$2 go build -o leanote-$1-$2$suffix ../app/tmp
|
||||
}
|
||||
|
||||
build "linux" "386";
|
||||
build "linux" "amd64";
|
||||
build "linux" "arm";
|
||||
|
||||
build "windows" "386";
|
||||
build "windows" "amd64";
|
||||
|
||||
# build "darwin" "386";
|
||||
build "darwin" "amd64";
|
||||
|
||||
|
||||
##======================
|
||||
# 2. release目录准备工作
|
||||
##======================
|
||||
rm -rf $tmp/leanote
|
||||
mkdir -p $tmp/leanote/app
|
||||
mkdir -p $tmp/leanote/conf
|
||||
mkdir -p $tmp/leanote/bin
|
||||
|
||||
##==================
|
||||
# 3. 复制
|
||||
##==================
|
||||
|
||||
cd $SP
|
||||
cd ../
|
||||
|
||||
# bin
|
||||
cp -r ./bin/src $tmp/leanote/bin/
|
||||
cp ./bin/run.sh $tmp/leanote/bin/
|
||||
cp ./bin/run.bat $tmp/leanote/bin/
|
||||
|
||||
# views
|
||||
cp -r ./app/views $tmp/leanote/app
|
||||
# 可不要, 源码
|
||||
#cp -r ./app/service $tmp/leanote/app/service
|
||||
#cp -r ./app/controllers $tmp/leanote/app/controllers
|
||||
#cp -r ./app/db $tmp/leanote/app/db
|
||||
#cp -r ./app/info $tmp/leanote/app/info
|
||||
#cp -r ./app/lea $tmp/leanote/app/lea
|
||||
|
||||
# conf
|
||||
cp ./conf/app.conf $tmp/leanote/conf/app.conf
|
||||
cp ./conf/routes $tmp/leanote/conf/routes
|
||||
# 处理app.conf, 还原配置
|
||||
cat $tmp/leanote/conf/app.conf | sed 's/db.dbname=leanote.*#/db.dbname=leanote #/' > $tmp/leanote/conf/app.conf2 # 不能直接覆盖
|
||||
rm $tmp/leanote/conf/app.conf
|
||||
mv $tmp/leanote/conf/app.conf2 $tmp/leanote/conf/app.conf
|
||||
|
||||
# others
|
||||
cp -r ./messages ./public ./mongodb_backup $tmp/leanote/
|
||||
|
||||
# delete some files
|
||||
rm -r $tmp/leanote/public/tinymce/classes
|
||||
rm -r $tmp/leanote/public/upload
|
||||
mkdir $tmp/leanote/public/upload
|
||||
rm -r $tmp/leanote/public/.codekit-cache
|
||||
rm $tmp/leanote/public/.DS_Store
|
||||
rm $tmp/leanote/public/config.codekit
|
||||
|
||||
# make link
|
||||
# cd $tmp/leanote/bin
|
||||
# ln -s ../../../../ ./src/github.com/leanote/leanote
|
||||
|
||||
# archieve
|
||||
# << 'BLOCK
|
||||
|
||||
##===========
|
||||
# 4. 打包
|
||||
##===========
|
||||
# $1 = linux
|
||||
# $2 = 386, amd64
|
||||
|
||||
# 创建一个$V的目录存放之
|
||||
rm -rf $tmp/$V
|
||||
mkdir $tmp/$V
|
||||
|
||||
function tarRelease()
|
||||
{
|
||||
echo tar-$1-$2
|
||||
cd $SP
|
||||
cd ../
|
||||
rm $tmp/leanote/bin/leanote-* # 删除之前的bin文件
|
||||
rm $tmp/leanote/bin/run* # 删除之前的run.sh 或 run.bat
|
||||
|
||||
if [ $1 = "linux" -o $1 = "darwin" ]
|
||||
then
|
||||
suffix=""
|
||||
if [ $2 = "arm" ]
|
||||
then
|
||||
cp ./bin/run-arm.sh $tmp/leanote/bin/run.sh
|
||||
else
|
||||
cp ./bin/run-$1-$2.sh $tmp/leanote/bin/run.sh
|
||||
fi
|
||||
else
|
||||
cp ./bin/run.bat $tmp/leanote/bin/
|
||||
suffix=".exe"
|
||||
fi
|
||||
|
||||
cp ./bin/leanote-$1-$2$suffix $tmp/leanote/bin/
|
||||
cd $tmp
|
||||
tar -cf $tmp/$V/leanote-$1-$2-$V.bin.tar leanote
|
||||
gzip $tmp/$V/leanote-$1-$2-$V.bin.tar
|
||||
}
|
||||
|
||||
tarRelease "linux" "386";
|
||||
tarRelease "linux" "amd64";
|
||||
tarRelease "linux" "arm";
|
||||
|
||||
tarRelease "windows" "386";
|
||||
tarRelease "windows" "amd64";
|
||||
|
||||
# tarRelease "darwin" "386";
|
||||
tarRelease "darwin" "amd64";
|
||||
|
||||
# BLOCK'
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
|
||||
|
||||
# set link
|
||||
|
||||
path="$SCRIPTPATH/src/github.com/leanote"
|
||||
if [ ! -d "$path" ]; then
|
||||
mkdir -p "$path"
|
||||
fi
|
||||
rm -rf $SCRIPTPATH/src/github.com/leanote/leanote # 先删除
|
||||
ln -s ../../../../ $SCRIPTPATH/src/github.com/leanote/leanote
|
||||
|
||||
# set GOPATH
|
||||
export GOPATH=$SCRIPTPATH
|
||||
|
||||
script="$SCRIPTPATH/leanote-linux-arm"
|
||||
chmod 777 $script
|
||||
$script -importPath github.com/leanote/leanote
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
|
||||
|
||||
# set link
|
||||
|
||||
path="$SCRIPTPATH/src/github.com/leanote"
|
||||
if [ ! -d "$path" ]; then
|
||||
mkdir -p "$path"
|
||||
fi
|
||||
rm -rf $SCRIPTPATH/src/github.com/leanote/leanote # 先删除
|
||||
ln -s ../../../../ $SCRIPTPATH/src/github.com/leanote/leanote
|
||||
|
||||
# set GOPATH
|
||||
export GOPATH=$SCRIPTPATH
|
||||
|
||||
script="$SCRIPTPATH/leanote-darwin-amd64"
|
||||
chmod 777 $script
|
||||
$script -importPath github.com/leanote/leanote
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
|
||||
|
||||
# set link
|
||||
|
||||
path="$SCRIPTPATH/src/github.com/leanote"
|
||||
if [ ! -d "$path" ]; then
|
||||
mkdir -p "$path"
|
||||
fi
|
||||
rm -rf $SCRIPTPATH/src/github.com/leanote/leanote # 先删除
|
||||
ln -s ../../../../ $SCRIPTPATH/src/github.com/leanote/leanote
|
||||
|
||||
# set GOPATH
|
||||
export GOPATH=$SCRIPTPATH
|
||||
|
||||
script="$SCRIPTPATH/leanote-linux-386"
|
||||
chmod 777 $script
|
||||
$script -importPath github.com/leanote/leanote
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
|
||||
|
||||
# set link
|
||||
|
||||
path="$SCRIPTPATH/src/github.com/leanote"
|
||||
if [ ! -d "$path" ]; then
|
||||
mkdir -p "$path"
|
||||
fi
|
||||
rm -rf $SCRIPTPATH/src/github.com/leanote/leanote # 先删除
|
||||
ln -s ../../../../ $SCRIPTPATH/src/github.com/leanote/leanote
|
||||
|
||||
# set GOPATH
|
||||
export GOPATH=$SCRIPTPATH
|
||||
|
||||
script="$SCRIPTPATH/leanote-linux-amd64"
|
||||
chmod 777 $script
|
||||
$script -importPath github.com/leanote/leanote
|
||||
23
bin/run.bat
23
bin/run.bat
@@ -1,23 +0,0 @@
|
||||
@echo off
|
||||
|
||||
cd..
|
||||
set SCRIPTPATH=%cd%
|
||||
|
||||
: top src directory
|
||||
set leanotePath="%SCRIPTPATH%\bin\src\github.com\leanote"
|
||||
|
||||
if not exist "%leanotePath%" mkdir "%leanotePath%"
|
||||
|
||||
: create software link
|
||||
if exist "%leanotePath%\leanote" del /Q "%leanotePath%\leanote"
|
||||
mklink /D "%leanotePath%\leanote" %SCRIPTPATH%
|
||||
|
||||
: set GOPATH
|
||||
set GOPATH="%SCRIPTPATH%\bin"
|
||||
|
||||
: run
|
||||
if %processor_architecture%==x86 (
|
||||
"%SCRIPTPATH%\bin\leanote-windows-386.exe" -importPath github.com/leanote/leanote
|
||||
) else (
|
||||
"%SCRIPTPATH%\bin\leanote-windows-amd64.exe" -importPath github.com/leanote/leanote
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
modules
|
||||
=======
|
||||
|
||||
Set of officially supported modules for Revel applications
|
||||
|
||||
### Caution
|
||||
this is a work in progress
|
||||
@@ -1,6 +0,0 @@
|
||||
modules/auth
|
||||
===============
|
||||
|
||||
Basic user/auth module
|
||||
|
||||
This should be modeled after [flask-security](https://github.com/mattupstate/flask-security)
|
||||
@@ -1,78 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
// "errors"
|
||||
)
|
||||
|
||||
var (
|
||||
Store StorageDriver
|
||||
)
|
||||
|
||||
// Store = gormauth.NewGormAuthDriver()
|
||||
|
||||
type UserAuth interface {
|
||||
// getters/setters implemented by the app-level model
|
||||
UserId() string
|
||||
Secret() string
|
||||
HashedSecret() string
|
||||
SetHashedSecret(string)
|
||||
|
||||
SecretDriver
|
||||
// // implemented by secret driver
|
||||
// Authenticate() (bool, error)
|
||||
}
|
||||
|
||||
type SecretDriver interface {
|
||||
Authenticate() (bool, error)
|
||||
HashSecret(args ...interface{}) (string, error)
|
||||
|
||||
// stuff for documentation
|
||||
// UserContext is expected in these?
|
||||
|
||||
// Secret expects 0 or non-0 arguments
|
||||
// When no parameter is passed, it acts as a getter
|
||||
// When one or more parameters are passed, it acts as a setter
|
||||
// A driver should specify the expected arguments and their meanings
|
||||
|
||||
// Register()
|
||||
// Login()
|
||||
// Logout()
|
||||
}
|
||||
|
||||
type StorageDriver interface {
|
||||
Save(user interface{}) error
|
||||
// Load should take a partially filled struct
|
||||
// (with values needed to look up)
|
||||
// and fills in the rest
|
||||
Load(user interface{}) error
|
||||
}
|
||||
|
||||
// func init() {
|
||||
// // auth.Store = gorm...
|
||||
// }
|
||||
|
||||
// func (c App) Login(email, password string) {
|
||||
|
||||
// u := User {
|
||||
// Email ...
|
||||
// }
|
||||
|
||||
// good, err := auth.Authenticate(email, password)
|
||||
|
||||
// user, err := user_info.GetUserByEmail(email)
|
||||
// }
|
||||
|
||||
// Bycrypt Authenticate() expects a single string argument of the plaintext password
|
||||
// It returns true on success and false if error or password mismatch
|
||||
// func Authenticate(attemptedUser UserAuth) (bool, error) {
|
||||
// // check user in Store
|
||||
// loadedUser, err := Store.Load(attemptedUser.UserId())
|
||||
// if err != nil {
|
||||
// return false, errors.New("User Not Found")
|
||||
// }
|
||||
|
||||
// loadedUser.Authenticate(attemptedUser.Secret())
|
||||
|
||||
// // successfully authenticated
|
||||
// return true, nil
|
||||
// }
|
||||
@@ -1,134 +0,0 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/modules/auth"
|
||||
"github.com/revel/modules/auth/driver/secret"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
email string
|
||||
password string
|
||||
hashpass string
|
||||
|
||||
secret.BcryptAuth // SecurityDriver for testing
|
||||
}
|
||||
|
||||
func NewUser(email, pass string) *User {
|
||||
u := &User{
|
||||
email: email,
|
||||
password: pass,
|
||||
}
|
||||
u.UserContext = u
|
||||
return u
|
||||
}
|
||||
|
||||
func (self *User) UserId() string {
|
||||
return self.email
|
||||
}
|
||||
|
||||
func (self *User) Secret() string {
|
||||
return self.password
|
||||
}
|
||||
|
||||
func (self *User) HashedSecret() string {
|
||||
return self.hashpass
|
||||
}
|
||||
|
||||
func (self *User) SetHashedSecret(hpass string) {
|
||||
self.hashpass = hpass
|
||||
}
|
||||
|
||||
// func (self *User) Load() string
|
||||
|
||||
type TestStore struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (self *TestStore) Save(user interface{}) error {
|
||||
u, ok := user.(*User)
|
||||
if !ok {
|
||||
return errors.New("TestStore.Save() expected arg of type User")
|
||||
}
|
||||
|
||||
hPass, err := u.HashSecret(u.Secret())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.data[u.UserId()] = hPass
|
||||
|
||||
return nil
|
||||
}
|
||||
func (self *TestStore) Load(user interface{}) error {
|
||||
u, ok := user.(*User)
|
||||
if !ok {
|
||||
return errors.New("TestStore.Load() expected arg of type User")
|
||||
}
|
||||
|
||||
hpass, ok := self.data[u.UserId()]
|
||||
if !ok {
|
||||
return errors.New("Record Not Found")
|
||||
}
|
||||
u.SetHashedSecret(hpass)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPasswordHash(t *testing.T) {
|
||||
auth.Store = &TestStore{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
u := NewUser("demo@domain.com", "demopass")
|
||||
fail := NewUser("demo@domain.com", "")
|
||||
|
||||
var err error
|
||||
u.hashpass, err = u.HashSecret(u.password)
|
||||
if err != nil {
|
||||
t.Errorf("Should have hashed password, get error: %v\n", err)
|
||||
}
|
||||
fail.hashpass, err = fail.HashSecret(fail.password)
|
||||
if err == nil {
|
||||
t.Errorf("Should have failed hashing\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
auth.Store = &TestStore{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
|
||||
// user registered a long time ago
|
||||
u := NewUser("demo@domain.com", "demopass")
|
||||
err := auth.Store.Save(u)
|
||||
if err != nil {
|
||||
t.Errorf("Should have saved user: %v", err)
|
||||
}
|
||||
|
||||
// users now logging in
|
||||
pass := NewUser("demo@domain.com", "demopass")
|
||||
fail := NewUser("demo@domain.com", "invalid")
|
||||
|
||||
// valid user is now trying to login
|
||||
// check user in DB
|
||||
err = auth.Store.Load(pass)
|
||||
if err != nil {
|
||||
t.Errorf("Should have loaded pass user: %v\n", err)
|
||||
}
|
||||
// check credentials
|
||||
ok, err := pass.Authenticate()
|
||||
if !ok || err != nil {
|
||||
t.Errorf("Should have authenticated user")
|
||||
}
|
||||
|
||||
// invalid user is now trying to login
|
||||
err = auth.Store.Load(fail)
|
||||
if err != nil {
|
||||
t.Errorf("Should have loaded fail user")
|
||||
}
|
||||
// this should fail
|
||||
ok, err = fail.Authenticate()
|
||||
if ok || err != nil {
|
||||
t.Errorf("Should have failed to authenticate user: %v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/* A basic user authentication module for Revel
|
||||
|
||||
list of concerns:
|
||||
- Separating out the interface and driver
|
||||
- Removing DB/Storage dependency
|
||||
- UUID as default identifier?
|
||||
- how to deal with password/secret or generally, method of authorization
|
||||
- default {views,controllers,routes} for register/login/logut ?
|
||||
- reset password in most basic ?
|
||||
- activation (and other features) in a second / more sophisticated driver
|
||||
- filter for checking that user is authenticated
|
||||
|
||||
|
||||
I think a driver is made up of 2 parts
|
||||
data prep and data storage
|
||||
register and password reset are part of data prep
|
||||
as is the auth hash method
|
||||
they don't care how the data is stored
|
||||
|
||||
then there is the data store
|
||||
|
||||
perhaps each auth user model should instantiate 2 drivers instead of 1?
|
||||
one for data prep components and one for storage
|
||||
so the security driver and the storage driver
|
||||
|
||||
*/
|
||||
package auth
|
||||
@@ -1,73 +0,0 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"github.com/revel/modules/auth"
|
||||
)
|
||||
|
||||
// example implementation of a Revel auth security driver
|
||||
// This driver should be embedded into your app-level User model
|
||||
// It expects your User model to have `Password` and `HashedPassword` string fields
|
||||
//
|
||||
// Your User model also needs to set itself as the UserContext for the BcryptAuth driver
|
||||
//
|
||||
// func NewUser(email, pass string) *User {
|
||||
// u := &User{
|
||||
// email: email,
|
||||
// password: pass,
|
||||
// }
|
||||
// u.UserContext = u
|
||||
// }
|
||||
//
|
||||
type BcryptAuth struct {
|
||||
UserContext auth.UserAuth
|
||||
}
|
||||
|
||||
// Bcrypt Secret() returns the hashed version of the password.
|
||||
// It expects an argument of type string, which is the plain text password
|
||||
func (self *BcryptAuth) HashSecret(args ...interface{}) (string, error) {
|
||||
if auth.Store == nil {
|
||||
return "", errors.New("Auth module StorageDriver not set")
|
||||
}
|
||||
argLen := len(args)
|
||||
if argLen == 0 {
|
||||
// we are getting
|
||||
return string(self.UserContext.HashedSecret()), nil
|
||||
}
|
||||
|
||||
if argLen == 1 {
|
||||
// we are setting
|
||||
password, ok := args[0].(string)
|
||||
if !ok {
|
||||
return "", errors.New("Wrong argument type provided, expected plaintext password as string")
|
||||
}
|
||||
hPass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
self.UserContext.SetHashedSecret(string(hPass))
|
||||
return self.UserContext.HashedSecret(), nil
|
||||
}
|
||||
|
||||
// bad argument count
|
||||
return "", errors.New("Too many arguments provided, expected one")
|
||||
}
|
||||
|
||||
// Bycrypt Authenticate() expects a single string argument of the plaintext password
|
||||
// It returns true on success and false if error or password mismatch
|
||||
func (self *BcryptAuth) Authenticate() (bool, error) {
|
||||
// check password
|
||||
err := bcrypt.CompareHashAndPassword([]byte(self.UserContext.HashedSecret()), []byte(self.UserContext.Secret()))
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// successfully authenticated
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
Mysql Auth driver
|
||||
==================
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
Postgresql Auth driver
|
||||
==================
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
Sqlite Auth driver
|
||||
==================
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package auth
|
||||
|
||||
// var storageDriver auth.StorageDriver // postgres in example
|
||||
|
||||
// type AuthUserModel struct {
|
||||
// userId string
|
||||
// security *SecurityDriver // bcrypt in example
|
||||
|
||||
// }
|
||||
|
||||
// func (self *AuthUserModel) UserId() string {
|
||||
// return self.userId
|
||||
// }
|
||||
|
||||
// func (self *AuthUserModel) Secret() string {
|
||||
// return self.security.Secret()
|
||||
// }
|
||||
@@ -1,121 +0,0 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"html/template"
|
||||
"io"
|
||||
"math"
|
||||
"net/url"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
// allowMethods are HTTP methods that do NOT require a token
|
||||
var allowedMethods = map[string]bool{
|
||||
"GET": true,
|
||||
"HEAD": true,
|
||||
"OPTIONS": true,
|
||||
"TRACE": true,
|
||||
}
|
||||
|
||||
func RandomString(length int) (string, error) {
|
||||
buffer := make([]byte, int(math.Ceil(float64(length)/2)))
|
||||
if _, err := io.ReadFull(rand.Reader, buffer); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
str := hex.EncodeToString(buffer)
|
||||
return str[:length], nil
|
||||
}
|
||||
|
||||
func RefreshToken(c *revel.Controller) {
|
||||
token, err := RandomString(64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Session["csrf_token"] = token
|
||||
}
|
||||
|
||||
// CsrfFilter enables CSRF request token creation and verification.
|
||||
//
|
||||
// Usage:
|
||||
// 1) Add `csrf.CsrfFilter` to the app's filters (it must come after the revel.SessionFilter).
|
||||
// 2) Add CSRF fields to a form with the template tag `{{ csrftoken . }}`. The filter adds a function closure to the `RenderArgs` that can pull out the secret and make the token as-needed, caching the value in the request. Ajax support provided through the `X-CSRFToken` header.
|
||||
func CsrfFilter(c *revel.Controller, fc []revel.Filter) {
|
||||
token, foundToken := c.Session["csrf_token"]
|
||||
|
||||
if !foundToken {
|
||||
RefreshToken(c)
|
||||
}
|
||||
|
||||
referer, refErr := url.Parse(c.Request.Header.Get("Referer"))
|
||||
isSameOrigin := sameOrigin(c.Request.URL, referer)
|
||||
|
||||
// If the Request method isn't in the white listed methods
|
||||
if !allowedMethods[c.Request.Method] && !IsExempt(c) {
|
||||
// Token wasn't present at all
|
||||
if !foundToken {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Session token missing.")
|
||||
return
|
||||
}
|
||||
|
||||
// Referer header is invalid
|
||||
if refErr != nil {
|
||||
c.Result = c.Forbidden("REVEL CSRF: HTTP Referer malformed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Same origin
|
||||
if !isSameOrigin {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Same origin mismatch.")
|
||||
return
|
||||
}
|
||||
|
||||
var requestToken string
|
||||
// First check for token in post data
|
||||
if c.Request.Method == "POST" {
|
||||
requestToken = c.Request.FormValue("csrftoken")
|
||||
}
|
||||
|
||||
// Then check for token in custom headers, as with AJAX
|
||||
if requestToken == "" {
|
||||
requestToken = c.Request.Header.Get("X-CSRFToken")
|
||||
}
|
||||
|
||||
if requestToken == "" || !compareToken(requestToken, token) {
|
||||
c.Result = c.Forbidden("REVEL CSRF: Invalid token.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fc[0](c, fc[1:])
|
||||
|
||||
// Only add token to RenderArgs if the request is: not AJAX, not missing referer header, and is same origin.
|
||||
if c.Request.Header.Get("X-CSRFToken") == "" && isSameOrigin {
|
||||
c.RenderArgs["_csrftoken"] = token
|
||||
}
|
||||
}
|
||||
|
||||
func compareToken(requestToken, token string) bool {
|
||||
// ConstantTimeCompare will panic if the []byte aren't the same length
|
||||
if len(requestToken) != len(token) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(requestToken), []byte(token)) == 1
|
||||
}
|
||||
|
||||
// Validates same origin policy
|
||||
func sameOrigin(u1, u2 *url.URL) bool {
|
||||
return u1.Scheme == u2.Scheme && u1.Host == u2.Host
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.TemplateFuncs["csrftoken"] = func(renderArgs map[string]interface{}) template.HTML {
|
||||
if tokenFunc, ok := renderArgs["_csrftoken"]; !ok {
|
||||
panic("REVEL CSRF: _csrftoken missing from RenderArgs.")
|
||||
} else {
|
||||
return template.HTML(tokenFunc.(func() string)())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var testFilters = []revel.Filter{
|
||||
CsrfFilter,
|
||||
func(c *revel.Controller, fc []revel.Filter) {
|
||||
c.RenderHtml("{{ csrftoken . }}")
|
||||
},
|
||||
}
|
||||
|
||||
func TestTokenInSession(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
getRequest, _ := http.NewRequest("GET", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(getRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if _, ok := c.Session["csrf_token"]; !ok {
|
||||
t.Fatal("token should be present in session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostWithoutToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("post without token should be forbidden")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoReferrer(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
|
||||
// and replace the old request
|
||||
c.Request = revel.NewRequest(formPostRequest)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("post without referer should be forbidden")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefererHttps(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "https://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
// and replace the old request
|
||||
c.Request = revel.NewRequest(formPostRequest)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status != 403 {
|
||||
t.Fatal("posts to https should have an https referer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderWithToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
formPostRequest.Header.Add("X-CSRFToken", token)
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
// and replace the old request
|
||||
c.Request = revel.NewRequest(formPostRequest)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post with http header token should be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormPostWithToken(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
RefreshToken(c)
|
||||
token := c.Session["csrf_token"]
|
||||
|
||||
// make a new request with the token
|
||||
data := url.Values{}
|
||||
data.Set("csrftoken", token)
|
||||
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", bytes.NewBufferString(data.Encode()))
|
||||
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||
formPostRequest.Header.Add("Referer", "http://www.example.com/")
|
||||
|
||||
// and replace the old request
|
||||
c.Request = revel.NewRequest(formPostRequest)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("form post with token should be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoTokenInArgsWhenCORs(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
getRequest, _ := http.NewRequest("GET", "http://www.example1.com/", nil)
|
||||
getRequest.Header.Add("Referer", "http://www.example2.com/")
|
||||
|
||||
c := revel.NewController(revel.NewRequest(getRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if _, ok := c.RenderArgs["_csrftoken"]; ok {
|
||||
t.Fatal("RenderArgs should not contain token when not same origin")
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var (
|
||||
exemptPath = make(map[string]bool)
|
||||
exemptAction = make(map[string]bool)
|
||||
)
|
||||
|
||||
func MarkExempt(route string) {
|
||||
if strings.HasPrefix(route, "/") {
|
||||
// e.g. "/controller/action"
|
||||
exemptPath[strings.ToLower(route)] = true
|
||||
} else if routeParts := strings.Split(route, "."); len(routeParts) == 2 {
|
||||
// e.g. "ControllerName.ActionName"
|
||||
exemptAction[route] = true
|
||||
} else {
|
||||
err := fmt.Sprintf("csrf.MarkExempt() received invalid argument \"%v\". Either provide a path prefixed with \"/\" or controller action in the form of \"ControllerName.ActionName\".", route)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func IsExempt(c *revel.Controller) bool {
|
||||
if _, ok := exemptPath[strings.ToLower(c.Request.Request.URL.Path)]; ok {
|
||||
return true
|
||||
} else if _, ok := exemptAction[c.Action]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func TestExemptPath(t *testing.T) {
|
||||
MarkExempt("/Controller/Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/Controller/Action", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemptPathCaseInsensitive(t *testing.T) {
|
||||
MarkExempt("/Controller/Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/controller/action", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemptAction(t *testing.T) {
|
||||
MarkExempt("Controller.Action")
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
postRequest, _ := http.NewRequest("POST", "http://www.example.com/Controller/Action", nil)
|
||||
c := revel.NewController(revel.NewRequest(postRequest), revel.NewResponse(resp))
|
||||
c.Session = make(revel.Session)
|
||||
c.Action = "Controller.Action"
|
||||
|
||||
testFilters[0](c, testFilters)
|
||||
|
||||
if c.Response.Status == 403 {
|
||||
t.Fatal("post to csrf exempt action should pass")
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// This module configures a database connection for the application.
|
||||
//
|
||||
// Developers use this module by importing and calling db.Init().
|
||||
// A "Transactional" controller type is provided as a way to import interceptors
|
||||
// that manage the transaction
|
||||
//
|
||||
// In particular, a transaction is begun before each request and committed on
|
||||
// success. If a panic occurred during the request, the transaction is rolled
|
||||
// back. (The application may also roll the transaction back itself.)
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
var (
|
||||
Db *sql.DB
|
||||
Driver string
|
||||
Spec string
|
||||
)
|
||||
|
||||
func Init() {
|
||||
// Read configuration.
|
||||
var found bool
|
||||
if Driver, found = revel.Config.String("db.driver"); !found {
|
||||
revel.ERROR.Fatal("No db.driver found.")
|
||||
}
|
||||
if Spec, found = revel.Config.String("db.spec"); !found {
|
||||
revel.ERROR.Fatal("No db.spec found.")
|
||||
}
|
||||
|
||||
// Open a connection.
|
||||
var err error
|
||||
Db, err = sql.Open(Driver, Spec)
|
||||
if err != nil {
|
||||
revel.ERROR.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type Transactional struct {
|
||||
*revel.Controller
|
||||
Txn *sql.Tx
|
||||
}
|
||||
|
||||
// Begin a transaction
|
||||
func (c *Transactional) Begin() revel.Result {
|
||||
txn, err := Db.Begin()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Txn = txn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback if it's still going (must have panicked).
|
||||
func (c *Transactional) Rollback() revel.Result {
|
||||
if c.Txn != nil {
|
||||
if err := c.Txn.Rollback(); err != nil {
|
||||
if err != sql.ErrTxDone {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
c.Txn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit the transaction.
|
||||
func (c *Transactional) Commit() revel.Result {
|
||||
if c.Txn != nil {
|
||||
if err := c.Txn.Commit(); err != nil {
|
||||
if err != sql.ErrTxDone {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
c.Txn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.InterceptMethod((*Transactional).Begin, revel.BEFORE)
|
||||
revel.InterceptMethod((*Transactional).Commit, revel.AFTER)
|
||||
revel.InterceptMethod((*Transactional).Rollback, revel.FINALLY)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/modules/jobs/app/jobs"
|
||||
"github.com/robfig/cron"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Jobs struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
func (c Jobs) Status() revel.Result {
|
||||
remoteAddress := c.Request.RemoteAddr
|
||||
if revel.Config.BoolDefault("jobs.acceptproxyaddress", false) {
|
||||
if proxiedAddress, isProxied := c.Request.Header["X-Forwarded-For"]; isProxied {
|
||||
remoteAddress = proxiedAddress[0]
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(remoteAddress, "127.0.0.1") && !strings.HasPrefix(remoteAddress, "::1") {
|
||||
return c.Forbidden("%s is not local", remoteAddress)
|
||||
}
|
||||
entries := jobs.MainCron.Entries()
|
||||
return c.Render(entries)
|
||||
}
|
||||
|
||||
func init() {
|
||||
revel.TemplateFuncs["castjob"] = func(job cron.Job) *jobs.Job {
|
||||
return job.(*jobs.Job)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/robfig/cron"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
Name string
|
||||
inner cron.Job
|
||||
status uint32
|
||||
running sync.Mutex
|
||||
}
|
||||
|
||||
const UNNAMED = "(unnamed)"
|
||||
|
||||
func New(job cron.Job) *Job {
|
||||
name := reflect.TypeOf(job).Name()
|
||||
if name == "Func" {
|
||||
name = UNNAMED
|
||||
}
|
||||
return &Job{
|
||||
Name: name,
|
||||
inner: job,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Job) Status() string {
|
||||
if atomic.LoadUint32(&j.status) > 0 {
|
||||
return "RUNNING"
|
||||
}
|
||||
return "IDLE"
|
||||
}
|
||||
|
||||
func (j *Job) Run() {
|
||||
// If the job panics, just print a stack trace.
|
||||
// Don't let the whole process die.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if revelError := revel.NewErrorFromPanic(err); revelError != nil {
|
||||
revel.ERROR.Print(err, "\n", revelError.Stack)
|
||||
} else {
|
||||
revel.ERROR.Print(err, "\n", string(debug.Stack()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if !selfConcurrent {
|
||||
j.running.Lock()
|
||||
defer j.running.Unlock()
|
||||
}
|
||||
|
||||
if workPermits != nil {
|
||||
workPermits <- struct{}{}
|
||||
defer func() { <-workPermits }()
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&j.status, 1)
|
||||
defer atomic.StoreUint32(&j.status, 0)
|
||||
|
||||
j.inner.Run()
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// A job runner for executing scheduled or ad-hoc tasks asynchronously from HTTP requests.
|
||||
//
|
||||
// It adds a couple of features on top of the cron package to make it play nicely with Revel:
|
||||
// 1. Protection against job panics. (They print to ERROR instead of take down the process)
|
||||
// 2. (Optional) Limit on the number of jobs that may run simulatenously, to
|
||||
// limit resource consumption.
|
||||
// 3. (Optional) Protection against multiple instances of a single job running
|
||||
// concurrently. If one execution runs into the next, the next will be queued.
|
||||
// 4. Cron expressions may be defined in app.conf and are reusable across jobs.
|
||||
// 5. Job status reporting.
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"github.com/robfig/cron"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Callers can use jobs.Func to wrap a raw func.
|
||||
// (Copying the type to this package makes it more visible)
|
||||
//
|
||||
// For example:
|
||||
// jobs.Schedule("cron.frequent", jobs.Func(myFunc))
|
||||
type Func func()
|
||||
|
||||
func (r Func) Run() { r() }
|
||||
|
||||
func Schedule(spec string, job cron.Job) error {
|
||||
// Look to see if given spec is a key from the Config.
|
||||
if strings.HasPrefix(spec, "cron.") {
|
||||
confSpec, found := revel.Config.String(spec)
|
||||
if !found {
|
||||
panic("Cron spec not found: " + spec)
|
||||
}
|
||||
spec = confSpec
|
||||
}
|
||||
sched, err := cron.Parse(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MainCron.Schedule(sched, New(job))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run the given job at a fixed interval.
|
||||
// The interval provided is the time between the job ending and the job being run again.
|
||||
// The time that the job takes to run is not included in the interval.
|
||||
func Every(duration time.Duration, job cron.Job) {
|
||||
MainCron.Schedule(cron.Every(duration), New(job))
|
||||
}
|
||||
|
||||
// Run the given job right now.
|
||||
func Now(job cron.Job) {
|
||||
go New(job).Run()
|
||||
}
|
||||
|
||||
// Run the given job once, after the given delay.
|
||||
func In(duration time.Duration, job cron.Job) {
|
||||
go func() {
|
||||
time.Sleep(duration)
|
||||
New(job).Run()
|
||||
}()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/revel/revel"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
const DEFAULT_JOB_POOL_SIZE = 10
|
||||
|
||||
var (
|
||||
// Singleton instance of the underlying job scheduler.
|
||||
MainCron *cron.Cron
|
||||
|
||||
// This limits the number of jobs allowed to run concurrently.
|
||||
workPermits chan struct{}
|
||||
|
||||
// Is a single job allowed to run concurrently with itself?
|
||||
selfConcurrent bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
MainCron = cron.New()
|
||||
revel.OnAppStart(func() {
|
||||
if size := revel.Config.IntDefault("jobs.pool", DEFAULT_JOB_POOL_SIZE); size > 0 {
|
||||
workPermits = make(chan struct{}, size)
|
||||
}
|
||||
selfConcurrent = revel.Config.BoolDefault("jobs.selfconcurrent", false)
|
||||
MainCron.Start()
|
||||
fmt.Println("Go to /@jobs to see job status.")
|
||||
})
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 12px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: none;
|
||||
}
|
||||
table td, table th {
|
||||
padding: 4 10px;
|
||||
border: none;
|
||||
}
|
||||
table tr:nth-child(odd) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Scheduled Jobs</h1>
|
||||
|
||||
<table>
|
||||
<tr><th>Name</th><th>Status</th><th>Last run</th><th>Next run</th></tr>
|
||||
{{range .entries}}
|
||||
{{$job := castjob .Job}}
|
||||
<tr>
|
||||
<td>{{$job.Name}}</td>
|
||||
<td>{{$job.Status}}</td>
|
||||
<td>{{if not .Prev.IsZero}}{{.Prev.Format "2006-01-02 15:04:05"}}{{end}}</td>
|
||||
<td>{{if not .Next.IsZero}}{{.Next.Format "2006-01-02 15:04:05"}}{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
@@ -1 +0,0 @@
|
||||
GET /@jobs Jobs.Status
|
||||
@@ -1,19 +0,0 @@
|
||||
Revel pprof module
|
||||
============
|
||||
|
||||
#### How to use:
|
||||
|
||||
1. Open your app.conf file and add the following line:
|
||||
`module.pprof=github.com/revel/modules/pprof`
|
||||
This will enable the pprof module.
|
||||
|
||||
2. Next, open your routes file and add:
|
||||
`module:pprof` **Note:** Do not change these routes. The pprof command-line tool by default assumes these routes.
|
||||
|
||||
Congrats! You can now profile your application. To use the web interface, visit `http://<host>:<port>/debug/pprof`. You can also use the `go tool pprof` command to profile your application and get a little deeper. Use the command by running `go tool pprof <binary> http://<host>:<port>`. For example, if you modified the booking sample, you would run: `go tool pprof $GOPATH/bin/booking http://localhost:9000` (assuming you used the default `revel run` arguments.
|
||||
|
||||
The command-line tool will take a 30-second CPU profile, and save the results to a temporary file in your `$HOME/pprof` directory. You can reference this file at a later time by using the same command as above, but by specifying the filename instead of the server address.
|
||||
|
||||
In order to fully utilize the command-line tool, you may need the graphviz utilities. If you're on OS X, you can install these easily using Homebrew: `brew install graphviz`.
|
||||
|
||||
To read more about profiling Go programs, here is some reading material: [The Go Blog](http://blog.golang.org/profiling-go-programs) : [net/pprof package documentation](http://golang.org/pkg/net/http/pprof/)
|
||||
@@ -1,39 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
)
|
||||
|
||||
type Pprof struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// The PprofHandler type makes it easy to call the net/http/pprof handler methods
|
||||
// since they all have the same method signature
|
||||
type PprofHandler func(http.ResponseWriter, *http.Request)
|
||||
|
||||
func (r PprofHandler) Apply(req *revel.Request, resp *revel.Response) {
|
||||
r(resp.Out, req.Request)
|
||||
}
|
||||
|
||||
func (c Pprof) Profile() revel.Result {
|
||||
return PprofHandler(pprof.Profile)
|
||||
}
|
||||
|
||||
func (c Pprof) Symbol() revel.Result {
|
||||
return PprofHandler(pprof.Symbol)
|
||||
}
|
||||
|
||||
func (c Pprof) Cmdline() revel.Result {
|
||||
return PprofHandler(pprof.Cmdline)
|
||||
}
|
||||
|
||||
func (c Pprof) Trace() revel.Result {
|
||||
return PprofHandler(pprof.Trace)
|
||||
}
|
||||
|
||||
func (c Pprof) Index() revel.Result {
|
||||
return PprofHandler(pprof.Index)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
GET /pprof/profile Pprof.Profile
|
||||
GET /pprof/symbol Pprof.Symbol
|
||||
GET /debug/pprof/cmdline Pprof.Cmdline
|
||||
GET /debug/pprof/trace Pprof.Trace
|
||||
GET /debug/pprof/* Pprof.Index
|
||||
GET /debug/pprof/ Pprof.Index
|
||||
@@ -1,121 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/revel/revel"
|
||||
"os"
|
||||
fpath "path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type Static struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// This method handles requests for files. The supplied prefix may be absolute
|
||||
// or relative. If the prefix is relative it is assumed to be relative to the
|
||||
// application directory. The filepath may either be just a file or an
|
||||
// additional filepath to search for the given file. This response may return
|
||||
// the following responses in the event of an error or invalid request;
|
||||
// 403(Forbidden): If the prefix filepath combination results in a directory.
|
||||
// 404(Not found): If the prefix and filepath combination results in a non-existent file.
|
||||
// 500(Internal Server Error): There are a few edge cases that would likely indicate some configuration error outside of revel.
|
||||
//
|
||||
// Note that when defining routes in routes/conf the parameters must not have
|
||||
// spaces around the comma.
|
||||
// Bad: Static.Serve("public/img", "favicon.png")
|
||||
// Good: Static.Serve("public/img","favicon.png")
|
||||
//
|
||||
// Examples:
|
||||
// Serving a directory
|
||||
// Route (conf/routes):
|
||||
// GET /public/{<.*>filepath} Static.Serve("public")
|
||||
// Request:
|
||||
// public/js/sessvars.js
|
||||
// Calls
|
||||
// Static.Serve("public","js/sessvars.js")
|
||||
//
|
||||
// Serving a file
|
||||
// Route (conf/routes):
|
||||
// GET /favicon.ico Static.Serve("public/img","favicon.png")
|
||||
// Request:
|
||||
// favicon.ico
|
||||
// Calls:
|
||||
// Static.Serve("public/img", "favicon.png")
|
||||
func (c Static) Serve(prefix, filepath string) revel.Result {
|
||||
// Fix for #503.
|
||||
prefix = c.Params.Fixed.Get("prefix")
|
||||
if prefix == "" {
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
return serve(c, prefix, filepath)
|
||||
}
|
||||
|
||||
// This method allows modules to serve binary files. The parameters are the same
|
||||
// as Static.Serve with the additional module name pre-pended to the list of
|
||||
// arguments.
|
||||
func (c Static) ServeModule(moduleName, prefix, filepath string) revel.Result {
|
||||
// Fix for #503.
|
||||
prefix = c.Params.Fixed.Get("prefix")
|
||||
if prefix == "" {
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
var basePath string
|
||||
for _, module := range revel.Modules {
|
||||
if module.Name == moduleName {
|
||||
basePath = module.Path
|
||||
}
|
||||
}
|
||||
|
||||
absPath := fpath.Join(basePath, fpath.FromSlash(prefix))
|
||||
|
||||
return serve(c, absPath, filepath)
|
||||
}
|
||||
|
||||
|
||||
// This method allows static serving of application files in a verified manner.
|
||||
func serve(c Static, prefix, filepath string) revel.Result {
|
||||
var basePath string
|
||||
if !fpath.IsAbs(prefix) {
|
||||
basePath = revel.BasePath
|
||||
}
|
||||
|
||||
basePathPrefix := fpath.Join(basePath, fpath.FromSlash(prefix))
|
||||
fname := fpath.Join(basePathPrefix, fpath.FromSlash(filepath))
|
||||
// Verify the request file path is within the application's scope of access
|
||||
if !strings.HasPrefix(fname, basePathPrefix) {
|
||||
revel.WARN.Printf("Attempted to read file outside of base path: %s", fname)
|
||||
return c.NotFound("")
|
||||
}
|
||||
|
||||
// Verify file path is accessible
|
||||
finfo, err := os.Stat(fname)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || err.(*os.PathError).Err == syscall.ENOTDIR {
|
||||
revel.WARN.Printf("File not found (%s): %s ", fname, err)
|
||||
return c.NotFound("File not found")
|
||||
}
|
||||
revel.ERROR.Printf("Error trying to get fileinfo for '%s': %s", fname, err)
|
||||
return c.RenderError(err)
|
||||
}
|
||||
|
||||
// Disallow directory listing
|
||||
if finfo.Mode().IsDir() {
|
||||
revel.WARN.Printf("Attempted directory listing of %s", fname)
|
||||
return c.Forbidden("Directory listing not allowed")
|
||||
}
|
||||
|
||||
// Open request file path
|
||||
file, err := os.Open(fname)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
revel.WARN.Printf("File not found (%s): %s ", fname, err)
|
||||
return c.NotFound("File not found")
|
||||
}
|
||||
revel.ERROR.Printf("Error opening '%s': %s", fname, err)
|
||||
return c.RenderError(err)
|
||||
}
|
||||
return c.RenderFile(file, revel.Inline)
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/revel/revel"
|
||||
"github.com/revel/revel/testing"
|
||||
)
|
||||
|
||||
// TestRunner is a controller which is used for running application tests in browser.
|
||||
type TestRunner struct {
|
||||
*revel.Controller
|
||||
}
|
||||
|
||||
// TestSuiteDesc is used for storing information about a single test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestSuiteDesc struct {
|
||||
Name string
|
||||
Tests []TestDesc
|
||||
|
||||
// Elem is reflect.Type which can be used for accessing methods
|
||||
// of the test suite.
|
||||
Elem reflect.Type
|
||||
}
|
||||
|
||||
// TestDesc is used for describing a single test of some test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestDesc struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// TestSuiteResult stores the results the whole test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestSuiteResult struct {
|
||||
Name string
|
||||
Passed bool
|
||||
Results []TestResult
|
||||
}
|
||||
|
||||
// TestResult represents the results of running a single test of some test suite.
|
||||
// This structure is required by revel test cmd.
|
||||
type TestResult struct {
|
||||
Name string
|
||||
Passed bool
|
||||
ErrorHTML template.HTML
|
||||
ErrorSummary string
|
||||
}
|
||||
|
||||
var (
|
||||
testSuites []TestSuiteDesc // A list of all available tests.
|
||||
|
||||
none = []reflect.Value{} // It is used as input for reflect call in a few places.
|
||||
|
||||
// registeredTests simplifies the search of test suites by their name.
|
||||
// "TestSuite.TestName" is used as a key. Value represents index in testSuites.
|
||||
registeredTests map[string]int
|
||||
)
|
||||
|
||||
/*
|
||||
Controller's action methods are below.
|
||||
*/
|
||||
|
||||
// Index is an action which renders the full list of available test suites and their tests.
|
||||
func (c TestRunner) Index() revel.Result {
|
||||
return c.Render(testSuites)
|
||||
}
|
||||
|
||||
// Run runs a single test, given by the argument.
|
||||
func (c TestRunner) Run(suite, test string) revel.Result {
|
||||
// Check whether requested test exists.
|
||||
suiteIndex, ok := registeredTests[suite+"."+test]
|
||||
if !ok {
|
||||
return c.NotFound("Test %s.%s does not exist", suite, test)
|
||||
}
|
||||
|
||||
result := TestResult{Name: test}
|
||||
|
||||
// Found the suite, create a new instance and run the named method.
|
||||
t := testSuites[suiteIndex].Elem
|
||||
v := reflect.New(t)
|
||||
func() {
|
||||
// When the function stops executing try to recover from panic.
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// If panic error is empty, exit.
|
||||
panicErr := revel.NewErrorFromPanic(err)
|
||||
if panicErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, prepare and format the response of server if possible.
|
||||
testSuite := v.Elem().FieldByName("TestSuite").Interface().(testing.TestSuite)
|
||||
res := formatResponse(testSuite)
|
||||
|
||||
// Render the error and save to the result structure.
|
||||
var buffer bytes.Buffer
|
||||
tmpl, _ := revel.MainTemplateLoader.Template("TestRunner/FailureDetail.html")
|
||||
tmpl.Render(&buffer, map[string]interface{}{
|
||||
"error": panicErr,
|
||||
"response": res,
|
||||
"postfix": suite + "_" + test,
|
||||
})
|
||||
result.ErrorSummary = errorSummary(panicErr)
|
||||
result.ErrorHTML = template.HTML(buffer.String())
|
||||
}
|
||||
}()
|
||||
|
||||
// Initialize the test suite with a NewTestSuite()
|
||||
testSuiteInstance := v.Elem().FieldByName("TestSuite")
|
||||
testSuiteInstance.Set(reflect.ValueOf(testing.NewTestSuite()))
|
||||
|
||||
// Make sure After method will be executed at the end.
|
||||
if m := v.MethodByName("After"); m.IsValid() {
|
||||
defer m.Call(none)
|
||||
}
|
||||
|
||||
// Start from running Before method of test suite if exists.
|
||||
if m := v.MethodByName("Before"); m.IsValid() {
|
||||
m.Call(none)
|
||||
}
|
||||
|
||||
// Start the test method itself.
|
||||
v.MethodByName(test).Call(none)
|
||||
|
||||
// No panic means success.
|
||||
result.Passed = true
|
||||
}()
|
||||
|
||||
return c.RenderJson(result)
|
||||
}
|
||||
|
||||
// List returns a JSON list of test suites and tests.
|
||||
// It is used by revel test command line tool.
|
||||
func (c TestRunner) List() revel.Result {
|
||||
return c.RenderJson(testSuites)
|
||||
}
|
||||
|
||||
/*
|
||||
Below are helper functions.
|
||||
*/
|
||||
|
||||
// describeSuite expects testsuite interface as input parameter
|
||||
// and returns its description in a form of TestSuiteDesc structure.
|
||||
func describeSuite(testSuite interface{}) TestSuiteDesc {
|
||||
t := reflect.TypeOf(testSuite)
|
||||
|
||||
// Get a list of methods of the embedded test type.
|
||||
// It will be used to make sure the same tests are not included in multiple test suites.
|
||||
super := t.Elem().Field(0).Type
|
||||
superMethods := map[string]bool{}
|
||||
for i := 0; i < super.NumMethod(); i++ {
|
||||
// Save the current method's name.
|
||||
superMethods[super.Method(i).Name] = true
|
||||
}
|
||||
|
||||
// Get a list of methods on the test suite that take no parameters, return
|
||||
// no results, and were not part of the embedded type's method set.
|
||||
var tests []TestDesc
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
m := t.Method(i)
|
||||
mt := m.Type
|
||||
|
||||
// Make sure the test method meets the criterias:
|
||||
// - method of testSuite without input parameters;
|
||||
// - nothing is returned;
|
||||
// - has "Test" prefix;
|
||||
// - doesn't belong to the embedded structure.
|
||||
methodWithoutParams := (mt.NumIn() == 1 && mt.In(0) == t)
|
||||
nothingReturned := (mt.NumOut() == 0)
|
||||
hasTestPrefix := (strings.HasPrefix(m.Name, "Test"))
|
||||
if methodWithoutParams && nothingReturned && hasTestPrefix && !superMethods[m.Name] {
|
||||
// Register the test suite's index so we can quickly find it by test's name later.
|
||||
registeredTests[t.Elem().Name()+"."+m.Name] = len(testSuites)
|
||||
|
||||
// Add test to the list of tests.
|
||||
tests = append(tests, TestDesc{m.Name})
|
||||
}
|
||||
}
|
||||
|
||||
return TestSuiteDesc{
|
||||
Name: t.Elem().Name(),
|
||||
Tests: tests,
|
||||
Elem: t.Elem(),
|
||||
}
|
||||
}
|
||||
|
||||
// errorSummary gets an error and returns its summary in human readable format.
|
||||
func errorSummary(err *revel.Error) (message string) {
|
||||
message = fmt.Sprintf("%4sStatus: %s\n%4sIn %s", "", err.Description, "", err.Path)
|
||||
|
||||
// If line of error isn't known return the message as is.
|
||||
if err.Line == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, include info about the line number and the relevant
|
||||
// source code lines.
|
||||
message += fmt.Sprintf(" (around line %d): ", err.Line)
|
||||
for _, line := range err.ContextSource() {
|
||||
if line.IsError {
|
||||
message += line.Source
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// formatResponse gets *revel.TestSuite as input parameter and
|
||||
// transform response related info into a readable format.
|
||||
func formatResponse(t testing.TestSuite) map[string]string {
|
||||
if t.Response == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
// Beautify the response JSON to make it human readable.
|
||||
resp, err := json.MarshalIndent(t.Response, "", " ")
|
||||
if err != nil {
|
||||
revel.ERROR.Println(err)
|
||||
}
|
||||
|
||||
// Remove extra new line symbols so they do not take too much space on a result page.
|
||||
// Allow no more than 1 line break at a time.
|
||||
body := strings.Replace(string(t.ResponseBody), "\n\n", "\n", -1)
|
||||
body = strings.Replace(body, "\r\n\r\n", "\r\n", -1)
|
||||
|
||||
return map[string]string{
|
||||
"Headers": string(resp),
|
||||
"Body": strings.TrimSpace(body),
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Every time app is restarted convert the list of available test suites
|
||||
// provided by the revel testing package into a format which will be used by
|
||||
// the testrunner module and revel test cmd.
|
||||
revel.OnAppStart(func() {
|
||||
// Extracting info about available test suites from revel/testing package.
|
||||
registeredTests = map[string]int{}
|
||||
for _, testSuite := range testing.TestSuites {
|
||||
testSuites = append(testSuites, describeSuite(testSuite))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/revel/revel"
|
||||
)
|
||||
|
||||
func init() {
|
||||
revel.OnAppStart(func() {
|
||||
fmt.Println("Go to /@tests to run the tests.")
|
||||
})
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<b>{{.error.Description}}</b>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active"><a href="#error_{{.postfix}}" role="tab" data-toggle="tab">Error</a></li>
|
||||
<li><a href="#stack_{{.postfix}}" role="tab" data-toggle="tab">Stack</a></li>
|
||||
{{if .response}}
|
||||
<li><a href="#headers_{{.postfix}}" role="tab" data-toggle="tab">Headers</a></li>
|
||||
<li><a href="#body_{{.postfix}}" role="tab" data-toggle="tab">Response Body</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="result_{{.postfix}}">
|
||||
<div class="tab-pane active" id="error_{{.postfix}}">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
In {{.error.Path}}{{if .error.Line}} (around {{if .error.Line}}line {{.error.Line}}{{end}}{{if .error.Column}} column {{.error.Column}}{{end}}){{end}}:
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{range .error.ContextSource}}
|
||||
{{if .IsError}}
|
||||
<pre><code class="go">{{.Source}}</code></pre>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="stack_{{.postfix}}">
|
||||
<pre><code class="bash">{{.error.Stack}}</code></pre>
|
||||
</div>
|
||||
{{if .response}}
|
||||
<div class="tab-pane" id="headers_{{.postfix}}">
|
||||
<pre><code class="json">{{.response.Headers}}</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane" id="body_{{.postfix}}">
|
||||
<pre><code class="html">{{.response.Body}}</code></pre>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,110 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Revel Test Runner</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<link href="{{url `Root`}}/@tests/public/css/bootstrap.min.css" type="text/css" rel="stylesheet"></link>
|
||||
<link href="{{url `Root`}}/@tests/public/css/github.css" type="text/css" rel="stylesheet"></link>
|
||||
<script src="{{url `Root`}}/@tests/public/js/jquery-1.9.1.min.js" type="text/javascript"></script>
|
||||
<script src="{{url `Root`}}/@tests/public/js/bootstrap.min.js" type="text/javascript"></script>
|
||||
<script src="{{url `Root`}}/@tests/public/js/highlight.pack.js" type="text/javascript"></script>
|
||||
<style>
|
||||
header { padding:20px 0; background-color:#ADD8E6 }
|
||||
.passed td { background-color: #90EE90 !important; }
|
||||
.failed td { background-color: #FFB6C1 !important; }
|
||||
.tests td.name, .tests td.result { padding-top: 13px; }
|
||||
pre { font-size:10px; white-space: pre; }
|
||||
.name { width: 25%; }
|
||||
.w100 { width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<table class="w100"><tr><td>
|
||||
<h1>Test Runner</h1>
|
||||
<p class="lead">Run all of your application's tests from here.</p>
|
||||
</td><td style="padding-left:150px;" class="text-right">
|
||||
<button class="btn btn-lg btn-success" all-tests="">Run All Tests</button>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
{{range .testSuites}}
|
||||
<p class="lead" style="margin-top:20px;">{{.Name}}</p>
|
||||
<table class="table table-striped tests" suite="{{.Name}}">
|
||||
{{range .Tests}}
|
||||
<tr>
|
||||
<td class="name">{{.Name}}</td>
|
||||
<td class="result">
|
||||
</td>
|
||||
<td class="text-right"><button test="{{.Name}}" class="btn btn-success">Run</button></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var buttons = [];
|
||||
var running;
|
||||
|
||||
$("button[test]").click(function() {
|
||||
var button = $(this).addClass("disabled").text("Running");
|
||||
addToQueue(button);
|
||||
});
|
||||
|
||||
$("button[all-tests]").click(function() {
|
||||
var button = $(this).addClass("disabled").text("Running");
|
||||
$("button[test]").click();
|
||||
});
|
||||
|
||||
function addToQueue(button) {
|
||||
buttons.push(button);
|
||||
if (!running) {
|
||||
running = true;
|
||||
nextTest();
|
||||
}
|
||||
}
|
||||
|
||||
function nextTest() {
|
||||
if (buttons.length == 0) {
|
||||
running = false;
|
||||
} else {
|
||||
var next = buttons.shift();
|
||||
runTest(next);
|
||||
}
|
||||
}
|
||||
|
||||
function runTest(button) {
|
||||
var suite = button.parents("table").attr("suite");
|
||||
var test = button.attr("test");
|
||||
var row = button.parents("tr");
|
||||
var resultCell = row.children(".result");
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
url: "{{url `Root`}}/@tests/"+suite+"/"+test,
|
||||
success: function(result) {
|
||||
row.attr("class", result.Passed ? "passed" : "failed");
|
||||
if (result.Passed) {
|
||||
resultCell.html("");
|
||||
} else {
|
||||
resultCell.html(result.ErrorHTML);
|
||||
|
||||
$("#result_" + suite + "_" + test + " pre code").each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
button.removeClass("disabled").text("Run");
|
||||
if (buttons.length == 0) {
|
||||
$("button[all-tests]").removeClass("disabled").text("Run All Tests");
|
||||
}
|
||||
nextTest();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,48 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Revel Test Runner</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
header { padding:20px 0; }
|
||||
header.passed { background-color: #90EE90 !important; }
|
||||
header.failed { background-color: #FFB6C1 !important; }
|
||||
table { margin-top: 20px; padding: 8px; line-height: 20px; }
|
||||
td { vertical-align: top; padding-right:20px; }
|
||||
a { color: #0088cc; }
|
||||
.container { margin-left: auto; margin-right: auto; width: 940px; overflow: hidden; }
|
||||
.result h2 { font-size: 16px; border-bottom: 1px solid #f0f0f0; padding-bottom: 0.2em; }
|
||||
.result.failed b { font-weight:bold; color: #C00; font-size: 14px; }
|
||||
.result.failed h2 { color: #C00; }
|
||||
.result .info { font-size: 12px; }
|
||||
.result .info pre { overflow: auto; background-color: #f0f0f0; width: 100%; max-height: 500px; }
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="{{if .Passed}}passed{{else}}failed{{end}}">
|
||||
<div class="container">
|
||||
<h1>{{.Name}}</h1>
|
||||
<p>{{if .Passed}}PASSED{{else}}FAILED{{end}}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
{{range .Results}}
|
||||
<div class="result {{if .Passed}}passed{{else}}failed{{end}}">
|
||||
<div><h2>{{.Name}}</h2></div>
|
||||
<div class="info">{{if .ErrorHTML}}{{.ErrorHTML}}{{else}}PASSED{{end}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +0,0 @@
|
||||
GET /@tests TestRunner.Index
|
||||
GET /@tests.list TestRunner.List
|
||||
GET /@tests/public/*filepath Static.ServeModule(testrunner,public)
|
||||
GET /@tests/:suite/:test TestRunner.Run
|
||||
File diff suppressed because one or more lines are too long
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #333;
|
||||
background: #f8f8f8;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-template_comment,
|
||||
.diff .hljs-header,
|
||||
.hljs-javadoc {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.css .rule .hljs-keyword,
|
||||
.hljs-winutils,
|
||||
.javascript .hljs-title,
|
||||
.nginx .hljs-title,
|
||||
.hljs-subst,
|
||||
.hljs-request,
|
||||
.hljs-status {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-hexcolor,
|
||||
.ruby .hljs-constant {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-tag .hljs-value,
|
||||
.hljs-phpdoc,
|
||||
.hljs-dartdoc,
|
||||
.tex .hljs-formula {
|
||||
color: #d14;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-id,
|
||||
.scss .hljs-preprocessor {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.javascript .hljs-title,
|
||||
.hljs-list .hljs-keyword,
|
||||
.hljs-subst {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-type,
|
||||
.vhdl .hljs-literal,
|
||||
.tex .hljs-command {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-tag .hljs-title,
|
||||
.hljs-rules .hljs-property,
|
||||
.django .hljs-tag .hljs-keyword {
|
||||
color: #000080;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-attribute,
|
||||
.hljs-variable,
|
||||
.lisp .hljs-body {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-regexp {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.ruby .hljs-symbol .hljs-string,
|
||||
.lisp .hljs-keyword,
|
||||
.clojure .hljs-keyword,
|
||||
.scheme .hljs-keyword,
|
||||
.tex .hljs-special,
|
||||
.hljs-prompt {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.hljs-built_in {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-preprocessor,
|
||||
.hljs-pragma,
|
||||
.hljs-pi,
|
||||
.hljs-doctype,
|
||||
.hljs-shebang,
|
||||
.hljs-cdata {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.diff .hljs-change {
|
||||
background: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-chunk {
|
||||
color: #aaa;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,545 +0,0 @@
|
||||
3dm=x-world/x-3dmf
|
||||
3dmf=x-world/x-3dmf
|
||||
7z=application/x-7z-compressed
|
||||
a=application/octet-stream
|
||||
aab=application/x-authorware-bin
|
||||
aam=application/x-authorware-map
|
||||
aas=application/x-authorware-seg
|
||||
abc=text/vndabc
|
||||
ace=application/x-ace-compressed
|
||||
acgi=text/html
|
||||
afl=video/animaflex
|
||||
ai=application/postscript
|
||||
aif=audio/aiff
|
||||
aifc=audio/aiff
|
||||
aiff=audio/aiff
|
||||
aim=application/x-aim
|
||||
aip=text/x-audiosoft-intra
|
||||
alz=application/x-alz-compressed
|
||||
ani=application/x-navi-animation
|
||||
aos=application/x-nokia-9000-communicator-add-on-software
|
||||
aps=application/mime
|
||||
arc=application/x-arc-compressed
|
||||
arj=application/arj
|
||||
art=image/x-jg
|
||||
asf=video/x-ms-asf
|
||||
asm=text/x-asm
|
||||
asp=text/asp
|
||||
asx=application/x-mplayer2
|
||||
au=audio/basic
|
||||
avi=video/x-msvideo
|
||||
avs=video/avs-video
|
||||
bcpio=application/x-bcpio
|
||||
bin=application/mac-binary
|
||||
bmp=image/bmp
|
||||
boo=application/book
|
||||
book=application/book
|
||||
boz=application/x-bzip2
|
||||
bsh=application/x-bsh
|
||||
bz2=application/x-bzip2
|
||||
bz=application/x-bzip
|
||||
c++=text/plain
|
||||
c=text/x-c
|
||||
cab=application/vnd.ms-cab-compressed
|
||||
cat=application/vndms-pkiseccat
|
||||
cc=text/x-c
|
||||
ccad=application/clariscad
|
||||
cco=application/x-cocoa
|
||||
cdf=application/cdf
|
||||
cer=application/pkix-cert
|
||||
cha=application/x-chat
|
||||
chat=application/x-chat
|
||||
chrt=application/vnd.kde.kchart
|
||||
class=application/java
|
||||
# ? class=application/java-vm
|
||||
com=text/plain
|
||||
conf=text/plain
|
||||
cpio=application/x-cpio
|
||||
cpp=text/x-c
|
||||
cpt=application/mac-compactpro
|
||||
crl=application/pkcs-crl
|
||||
crt=application/pkix-cert
|
||||
crx=application/x-chrome-extension
|
||||
csh=text/x-scriptcsh
|
||||
css=text/css
|
||||
csv=text/csv
|
||||
cxx=text/plain
|
||||
dar=application/x-dar
|
||||
dcr=application/x-director
|
||||
deb=application/x-debian-package
|
||||
deepv=application/x-deepv
|
||||
def=text/plain
|
||||
der=application/x-x509-ca-cert
|
||||
dif=video/x-dv
|
||||
dir=application/x-director
|
||||
divx=video/divx
|
||||
dl=video/dl
|
||||
dmg=application/x-apple-diskimage
|
||||
doc=application/msword
|
||||
dot=application/msword
|
||||
dp=application/commonground
|
||||
drw=application/drafting
|
||||
dump=application/octet-stream
|
||||
dv=video/x-dv
|
||||
dvi=application/x-dvi
|
||||
dwf=drawing/x-dwf=(old)
|
||||
dwg=application/acad
|
||||
dxf=application/dxf
|
||||
dxr=application/x-director
|
||||
el=text/x-scriptelisp
|
||||
elc=application/x-bytecodeelisp=(compiled=elisp)
|
||||
eml=message/rfc822
|
||||
env=application/x-envoy
|
||||
eps=application/postscript
|
||||
es=application/x-esrehber
|
||||
etx=text/x-setext
|
||||
evy=application/envoy
|
||||
exe=application/octet-stream
|
||||
f77=text/x-fortran
|
||||
f90=text/x-fortran
|
||||
f=text/x-fortran
|
||||
fdf=application/vndfdf
|
||||
fif=application/fractals
|
||||
fli=video/fli
|
||||
flo=image/florian
|
||||
flv=video/x-flv
|
||||
flx=text/vndfmiflexstor
|
||||
fmf=video/x-atomic3d-feature
|
||||
for=text/x-fortran
|
||||
fpx=image/vndfpx
|
||||
frl=application/freeloader
|
||||
funk=audio/make
|
||||
g3=image/g3fax
|
||||
g=text/plain
|
||||
gif=image/gif
|
||||
gl=video/gl
|
||||
gsd=audio/x-gsm
|
||||
gsm=audio/x-gsm
|
||||
gsp=application/x-gsp
|
||||
gss=application/x-gss
|
||||
gtar=application/x-gtar
|
||||
gz=application/x-compressed
|
||||
gzip=application/x-gzip
|
||||
h=text/x-h
|
||||
hdf=application/x-hdf
|
||||
help=application/x-helpfile
|
||||
hgl=application/vndhp-hpgl
|
||||
hh=text/x-h
|
||||
hlb=text/x-script
|
||||
hlp=application/hlp
|
||||
hpg=application/vndhp-hpgl
|
||||
hpgl=application/vndhp-hpgl
|
||||
hqx=application/binhex
|
||||
hta=application/hta
|
||||
htc=text/x-component
|
||||
htm=text/html
|
||||
html=text/html
|
||||
htmls=text/html
|
||||
htt=text/webviewhtml
|
||||
htx=text/html
|
||||
ice=x-conference/x-cooltalk
|
||||
ico=image/x-icon
|
||||
ics=text/calendar
|
||||
icz=text/calendar
|
||||
idc=text/plain
|
||||
ief=image/ief
|
||||
iefs=image/ief
|
||||
iges=application/iges
|
||||
igs=application/iges
|
||||
ima=application/x-ima
|
||||
imap=application/x-httpd-imap
|
||||
inf=application/inf
|
||||
ins=application/x-internett-signup
|
||||
ip=application/x-ip2
|
||||
isu=video/x-isvideo
|
||||
it=audio/it
|
||||
iv=application/x-inventor
|
||||
ivr=i-world/i-vrml
|
||||
ivy=application/x-livescreen
|
||||
jam=audio/x-jam
|
||||
jav=text/x-java-source
|
||||
java=text/x-java-source
|
||||
jcm=application/x-java-commerce
|
||||
jfif-tbnl=image/jpeg
|
||||
jfif=image/jpeg
|
||||
jnlp=application/x-java-jnlp-file
|
||||
jpe=image/jpeg
|
||||
jpeg=image/jpeg
|
||||
jpg=image/jpeg
|
||||
jps=image/x-jps
|
||||
js=application/javascript
|
||||
json=application/json
|
||||
jut=image/jutvision
|
||||
kar=audio/midi
|
||||
karbon=application/vnd.kde.karbon
|
||||
kfo=application/vnd.kde.kformula
|
||||
flw=application/vnd.kde.kivio
|
||||
kml=application/vnd.google-earth.kml+xml
|
||||
kmz=application/vnd.google-earth.kmz
|
||||
kon=application/vnd.kde.kontour
|
||||
kpr=application/vnd.kde.kpresenter
|
||||
kpt=application/vnd.kde.kpresenter
|
||||
ksp=application/vnd.kde.kspread
|
||||
kwd=application/vnd.kde.kword
|
||||
kwt=application/vnd.kde.kword
|
||||
ksh=text/x-scriptksh
|
||||
la=audio/nspaudio
|
||||
lam=audio/x-liveaudio
|
||||
latex=application/x-latex
|
||||
lha=application/lha
|
||||
lhx=application/octet-stream
|
||||
list=text/plain
|
||||
lma=audio/nspaudio
|
||||
log=text/plain
|
||||
lsp=text/x-scriptlisp
|
||||
lst=text/plain
|
||||
lsx=text/x-la-asf
|
||||
ltx=application/x-latex
|
||||
lzh=application/octet-stream
|
||||
lzx=application/lzx
|
||||
m1v=video/mpeg
|
||||
m2a=audio/mpeg
|
||||
m2v=video/mpeg
|
||||
m3u=audio/x-mpegurl
|
||||
m=text/x-m
|
||||
man=application/x-troff-man
|
||||
manifest=text/cache-manifest
|
||||
map=application/x-navimap
|
||||
mar=text/plain
|
||||
mbd=application/mbedlet
|
||||
mc$=application/x-magic-cap-package-10
|
||||
mcd=application/mcad
|
||||
mcf=text/mcf
|
||||
mcp=application/netmc
|
||||
me=application/x-troff-me
|
||||
mht=message/rfc822
|
||||
mhtml=message/rfc822
|
||||
mid=application/x-midi
|
||||
midi=application/x-midi
|
||||
mif=application/x-frame
|
||||
mime=message/rfc822
|
||||
mjf=audio/x-vndaudioexplosionmjuicemediafile
|
||||
mjpg=video/x-motion-jpeg
|
||||
mm=application/base64
|
||||
mme=application/base64
|
||||
mod=audio/mod
|
||||
moov=video/quicktime
|
||||
mov=video/quicktime
|
||||
movie=video/x-sgi-movie
|
||||
mp2=audio/mpeg
|
||||
mp3=audio/mpeg3
|
||||
mp4=video/mp4
|
||||
mpa=audio/mpeg
|
||||
mpc=application/x-project
|
||||
mpe=video/mpeg
|
||||
mpeg=video/mpeg
|
||||
mpg=video/mpeg
|
||||
mpga=audio/mpeg
|
||||
mpp=application/vndms-project
|
||||
mpt=application/x-project
|
||||
mpv=application/x-project
|
||||
mpx=application/x-project
|
||||
mrc=application/marc
|
||||
ms=application/x-troff-ms
|
||||
mv=video/x-sgi-movie
|
||||
my=audio/make
|
||||
mzz=application/x-vndaudioexplosionmzz
|
||||
nap=image/naplps
|
||||
naplps=image/naplps
|
||||
nc=application/x-netcdf
|
||||
ncm=application/vndnokiaconfiguration-message
|
||||
nif=image/x-niff
|
||||
niff=image/x-niff
|
||||
nix=application/x-mix-transfer
|
||||
nsc=application/x-conference
|
||||
nvd=application/x-navidoc
|
||||
o=application/octet-stream
|
||||
oda=application/oda
|
||||
odb=application/vnd.oasis.opendocument.database
|
||||
odc=application/vnd.oasis.opendocument.chart
|
||||
odf=application/vnd.oasis.opendocument.formula
|
||||
odg=application/vnd.oasis.opendocument.graphics
|
||||
odi=application/vnd.oasis.opendocument.image
|
||||
odm=application/vnd.oasis.opendocument.text-master
|
||||
odp=application/vnd.oasis.opendocument.presentation
|
||||
ods=application/vnd.oasis.opendocument.spreadsheet
|
||||
odt=application/vnd.oasis.opendocument.text
|
||||
oga=audio/ogg
|
||||
ogg=audio/ogg
|
||||
ogv=video/ogg
|
||||
omc=application/x-omc
|
||||
omcd=application/x-omcdatamaker
|
||||
omcr=application/x-omcregerator
|
||||
otc=application/vnd.oasis.opendocument.chart-template
|
||||
otf=application/vnd.oasis.opendocument.formula-template
|
||||
otg=application/vnd.oasis.opendocument.graphics-template
|
||||
oth=application/vnd.oasis.opendocument.text-web
|
||||
oti=application/vnd.oasis.opendocument.image-template
|
||||
otm=application/vnd.oasis.opendocument.text-master
|
||||
otp=application/vnd.oasis.opendocument.presentation-template
|
||||
ots=application/vnd.oasis.opendocument.spreadsheet-template
|
||||
ott=application/vnd.oasis.opendocument.text-template
|
||||
p10=application/pkcs10
|
||||
p12=application/pkcs-12
|
||||
p7a=application/x-pkcs7-signature
|
||||
p7c=application/pkcs7-mime
|
||||
p7m=application/pkcs7-mime
|
||||
p7r=application/x-pkcs7-certreqresp
|
||||
p7s=application/pkcs7-signature
|
||||
p=text/x-pascal
|
||||
part=application/pro_eng
|
||||
pas=text/pascal
|
||||
pbm=image/x-portable-bitmap
|
||||
pcl=application/vndhp-pcl
|
||||
pct=image/x-pict
|
||||
pcx=image/x-pcx
|
||||
pdb=chemical/x-pdb
|
||||
pdf=application/pdf
|
||||
pfunk=audio/make
|
||||
pgm=image/x-portable-graymap
|
||||
pic=image/pict
|
||||
pict=image/pict
|
||||
pkg=application/x-newton-compatible-pkg
|
||||
pko=application/vndms-pkipko
|
||||
pl=text/x-scriptperl
|
||||
plx=application/x-pixclscript
|
||||
pm4=application/x-pagemaker
|
||||
pm5=application/x-pagemaker
|
||||
pm=text/x-scriptperl-module
|
||||
png=image/png
|
||||
pnm=application/x-portable-anymap
|
||||
pot=application/mspowerpoint
|
||||
pov=model/x-pov
|
||||
ppa=application/vndms-powerpoint
|
||||
ppm=image/x-portable-pixmap
|
||||
pps=application/mspowerpoint
|
||||
ppt=application/mspowerpoint
|
||||
ppz=application/mspowerpoint
|
||||
pre=application/x-freelance
|
||||
prt=application/pro_eng
|
||||
ps=application/postscript
|
||||
psd=application/octet-stream
|
||||
pvu=paleovu/x-pv
|
||||
pwz=application/vndms-powerpoint
|
||||
py=text/x-scriptphyton
|
||||
pyc=applicaiton/x-bytecodepython
|
||||
qcp=audio/vndqcelp
|
||||
qd3=x-world/x-3dmf
|
||||
qd3d=x-world/x-3dmf
|
||||
qif=image/x-quicktime
|
||||
qt=video/quicktime
|
||||
qtc=video/x-qtc
|
||||
qti=image/x-quicktime
|
||||
qtif=image/x-quicktime
|
||||
ra=audio/x-pn-realaudio
|
||||
ram=audio/x-pn-realaudio
|
||||
rar=application/x-rar-compressed
|
||||
ras=application/x-cmu-raster
|
||||
rast=image/cmu-raster
|
||||
rexx=text/x-scriptrexx
|
||||
rf=image/vndrn-realflash
|
||||
rgb=image/x-rgb
|
||||
rm=application/vndrn-realmedia
|
||||
rmi=audio/mid
|
||||
rmm=audio/x-pn-realaudio
|
||||
rmp=audio/x-pn-realaudio
|
||||
rng=application/ringing-tones
|
||||
rnx=application/vndrn-realplayer
|
||||
roff=application/x-troff
|
||||
rp=image/vndrn-realpix
|
||||
rpm=audio/x-pn-realaudio-plugin
|
||||
rt=text/vndrn-realtext
|
||||
rtf=text/richtext
|
||||
rtx=text/richtext
|
||||
rv=video/vndrn-realvideo
|
||||
s=text/x-asm
|
||||
s3m=audio/s3m
|
||||
s7z=application/x-7z-compressed
|
||||
saveme=application/octet-stream
|
||||
sbk=application/x-tbook
|
||||
scm=text/x-scriptscheme
|
||||
sdml=text/plain
|
||||
sdp=application/sdp
|
||||
sdr=application/sounder
|
||||
sea=application/sea
|
||||
set=application/set
|
||||
sgm=text/x-sgml
|
||||
sgml=text/x-sgml
|
||||
sh=text/x-scriptsh
|
||||
shar=application/x-bsh
|
||||
shtml=text/x-server-parsed-html
|
||||
sid=audio/x-psid
|
||||
skd=application/x-koan
|
||||
skm=application/x-koan
|
||||
skp=application/x-koan
|
||||
skt=application/x-koan
|
||||
sit=application/x-stuffit
|
||||
sitx=application/x-stuffitx
|
||||
sl=application/x-seelogo
|
||||
smi=application/smil
|
||||
smil=application/smil
|
||||
snd=audio/basic
|
||||
sol=application/solids
|
||||
spc=text/x-speech
|
||||
spl=application/futuresplash
|
||||
spr=application/x-sprite
|
||||
sprite=application/x-sprite
|
||||
spx=audio/ogg
|
||||
src=application/x-wais-source
|
||||
ssi=text/x-server-parsed-html
|
||||
ssm=application/streamingmedia
|
||||
sst=application/vndms-pkicertstore
|
||||
step=application/step
|
||||
stl=application/sla
|
||||
stp=application/step
|
||||
sv4cpio=application/x-sv4cpio
|
||||
sv4crc=application/x-sv4crc
|
||||
svf=image/vnddwg
|
||||
svg=image/svg+xml
|
||||
svr=application/x-world
|
||||
swf=application/x-shockwave-flash
|
||||
t=application/x-troff
|
||||
talk=text/x-speech
|
||||
tar=application/x-tar
|
||||
tbk=application/toolbook
|
||||
tcl=text/x-scripttcl
|
||||
tcsh=text/x-scripttcsh
|
||||
tex=application/x-tex
|
||||
texi=application/x-texinfo
|
||||
texinfo=application/x-texinfo
|
||||
text=text/plain
|
||||
tgz=application/gnutar
|
||||
tif=image/tiff
|
||||
tiff=image/tiff
|
||||
tr=application/x-troff
|
||||
tsi=audio/tsp-audio
|
||||
tsp=application/dsptype
|
||||
tsv=text/tab-separated-values
|
||||
turbot=image/florian
|
||||
txt=text/plain
|
||||
uil=text/x-uil
|
||||
uni=text/uri-list
|
||||
unis=text/uri-list
|
||||
unv=application/i-deas
|
||||
uri=text/uri-list
|
||||
uris=text/uri-list
|
||||
ustar=application/x-ustar
|
||||
uu=text/x-uuencode
|
||||
uue=text/x-uuencode
|
||||
vcd=application/x-cdlink
|
||||
vcf=text/x-vcard
|
||||
vcard=text/x-vcard
|
||||
vcs=text/x-vcalendar
|
||||
vda=application/vda
|
||||
vdo=video/vdo
|
||||
vew=application/groupwise
|
||||
viv=video/vivo
|
||||
vivo=video/vivo
|
||||
vmd=application/vocaltec-media-desc
|
||||
vmf=application/vocaltec-media-file
|
||||
voc=audio/voc
|
||||
vos=video/vosaic
|
||||
vox=audio/voxware
|
||||
vqe=audio/x-twinvq-plugin
|
||||
vqf=audio/x-twinvq
|
||||
vql=audio/x-twinvq-plugin
|
||||
vrml=application/x-vrml
|
||||
vrt=x-world/x-vrt
|
||||
vsd=application/x-visio
|
||||
vst=application/x-visio
|
||||
vsw=application/x-visio
|
||||
w60=application/wordperfect60
|
||||
w61=application/wordperfect61
|
||||
w6w=application/msword
|
||||
wav=audio/wav
|
||||
wb1=application/x-qpro
|
||||
wbmp=image/vnd.wap.wbmp
|
||||
web=application/vndxara
|
||||
wiz=application/msword
|
||||
wk1=application/x-123
|
||||
wmf=windows/metafile
|
||||
wml=text/vnd.wap.wml
|
||||
wmlc=application/vnd.wap.wmlc
|
||||
wmls=text/vnd.wap.wmlscript
|
||||
wmlsc=application/vnd.wap.wmlscriptc
|
||||
word=application/msword
|
||||
wp5=application/wordperfect
|
||||
wp6=application/wordperfect
|
||||
wp=application/wordperfect
|
||||
wpd=application/wordperfect
|
||||
wq1=application/x-lotus
|
||||
wri=application/mswrite
|
||||
wrl=application/x-world
|
||||
wrz=model/vrml
|
||||
wsc=text/scriplet
|
||||
wsrc=application/x-wais-source
|
||||
wtk=application/x-wintalk
|
||||
x-png=image/png
|
||||
xbm=image/x-xbitmap
|
||||
xdr=video/x-amt-demorun
|
||||
xgz=xgl/drawing
|
||||
xif=image/vndxiff
|
||||
xl=application/excel
|
||||
xla=application/excel
|
||||
xlb=application/excel
|
||||
xlc=application/excel
|
||||
xld=application/excel
|
||||
xlk=application/excel
|
||||
xll=application/excel
|
||||
xlm=application/excel
|
||||
xls=application/excel
|
||||
xlt=application/excel
|
||||
xlv=application/excel
|
||||
xlw=application/excel
|
||||
xm=audio/xm
|
||||
xml=text/xml
|
||||
xmz=xgl/movie
|
||||
xpix=application/x-vndls-xpix
|
||||
xpm=image/x-xpixmap
|
||||
xsr=video/x-amt-showrun
|
||||
xwd=image/x-xwd
|
||||
xyz=chemical/x-pdb
|
||||
z=application/x-compress
|
||||
zip=application/zip
|
||||
zoo=application/octet-stream
|
||||
zsh=text/x-scriptzsh
|
||||
# Office 2007 mess - http://wdg.uncc.edu/Microsoft_Office_2007_MIME_Types_for_Apache_and_IIS
|
||||
docx=application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
docm=application/vnd.ms-word.document.macroEnabled.12
|
||||
dotx=application/vnd.openxmlformats-officedocument.wordprocessingml.template
|
||||
dotm=application/vnd.ms-word.template.macroEnabled.12
|
||||
xlsx=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
xlsm=application/vnd.ms-excel.sheet.macroEnabled.12
|
||||
xltx=application/vnd.openxmlformats-officedocument.spreadsheetml.template
|
||||
xltm=application/vnd.ms-excel.template.macroEnabled.12
|
||||
xlsb=application/vnd.ms-excel.sheet.binary.macroEnabled.12
|
||||
xlam=application/vnd.ms-excel.addin.macroEnabled.12
|
||||
pptx=application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||
pptm=application/vnd.ms-powerpoint.presentation.macroEnabled.12
|
||||
ppsx=application/vnd.openxmlformats-officedocument.presentationml.slideshow
|
||||
ppsm=application/vnd.ms-powerpoint.slideshow.macroEnabled.12
|
||||
potx=application/vnd.openxmlformats-officedocument.presentationml.template
|
||||
potm=application/vnd.ms-powerpoint.template.macroEnabled.12
|
||||
ppam=application/vnd.ms-powerpoint.addin.macroEnabled.12
|
||||
sldx=application/vnd.openxmlformats-officedocument.presentationml.slide
|
||||
sldm=application/vnd.ms-powerpoint.slide.macroEnabled.12
|
||||
thmx=application/vnd.ms-officetheme
|
||||
onetoc=application/onenote
|
||||
onetoc2=application/onenote
|
||||
onetmp=application/onenote
|
||||
onepkg=application/onenote
|
||||
# koffice
|
||||
|
||||
# iWork
|
||||
key=application/x-iwork-keynote-sffkey
|
||||
kth=application/x-iwork-keynote-sffkth
|
||||
nmbtemplate=application/x-iwork-numbers-sfftemplate
|
||||
numbers=application/x-iwork-numbers-sffnumbers
|
||||
pages=application/x-iwork-pages-sffpages
|
||||
template=application/x-iwork-pages-sfftemplate
|
||||
|
||||
# Extensions for Mozilla apps (Firefox and friends)
|
||||
xpi=application/x-xpinstall
|
||||
|
||||
# Opera extensions
|
||||
oex=application/x-opera-extension
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Forbidden</title>
|
||||
</head>
|
||||
<body>
|
||||
{{with .Error}}
|
||||
<h1>
|
||||
{{.Title}}
|
||||
</h1>
|
||||
<p>
|
||||
{{.Description}}
|
||||
</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"title": "{{js .Error.Title}}",
|
||||
"description": "{{js .Error.Description}}"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{{.Error.Title}}
|
||||
|
||||
{{.Error.Description}}
|
||||
@@ -1 +0,0 @@
|
||||
<forbidden>{{.Error.Description}}</forbidden>
|
||||
@@ -1,63 +0,0 @@
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Helvetica, Arial, Sans;
|
||||
background: #EEEEEE;
|
||||
}
|
||||
.block {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
#header h1 {
|
||||
font-weight: normal;
|
||||
font-size: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
#more {
|
||||
color: #666;
|
||||
font-size: 80%;
|
||||
border: none;
|
||||
}
|
||||
#header {
|
||||
background: #FFFFCC;
|
||||
}
|
||||
#header p {
|
||||
color: #333;
|
||||
}
|
||||
#routes {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
#routes h2 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
#routes ol {
|
||||
|
||||
}
|
||||
#routes li {
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="header" class="block">
|
||||
{{with .Error}}
|
||||
<h1>
|
||||
{{.Title}}
|
||||
</h1>
|
||||
<p>
|
||||
{{.Description}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="routes" class="block">
|
||||
<h2>These routes have been tried, in this order :</h2>
|
||||
<ol>
|
||||
{{range .Router.Routes}}
|
||||
<li>{{pad .Method 10}}{{pad .Path 50}}{{.Action}}</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
</div>
|
||||
@@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Not found</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{if .DevMode}}
|
||||
|
||||
{{template "errors/404-dev.html" .}}
|
||||
|
||||
{{else}}
|
||||
|
||||
{{with .Error}}
|
||||
<h1>
|
||||
{{.Title}}
|
||||
</h1>
|
||||
<p>
|
||||
{{.Description}}
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"title": "{{js .Error.Title}}",
|
||||
"description": "{{js .Error.Description}}"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{{.Error.Title}}
|
||||
|
||||
{{.Error.Description}}
|
||||
@@ -1 +0,0 @@
|
||||
<notfound>{{.Error.Description}}</notfound>
|
||||
@@ -1,118 +0,0 @@
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Helvetica, Arial, Sans;
|
||||
background: #EEEEEE;
|
||||
}
|
||||
.block {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
#header h1 {
|
||||
font-weight: normal;
|
||||
font-size: 28px;
|
||||
margin: 0;
|
||||
}
|
||||
#more {
|
||||
color: #666;
|
||||
font-size: 80%;
|
||||
border: none;
|
||||
}
|
||||
#header {
|
||||
background: #fcd2da;
|
||||
}
|
||||
#header p {
|
||||
color: #333;
|
||||
}
|
||||
#source {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
#source h2 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
#source .lineNumber {
|
||||
float: left;
|
||||
display: block;
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
#source .line {
|
||||
clear: both;
|
||||
color: #333;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
#source pre {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#source .error {
|
||||
color: #c00 !important;
|
||||
}
|
||||
#source .error .lineNumber {
|
||||
background: #c00;
|
||||
}
|
||||
#source a {
|
||||
text-decoration: none;
|
||||
}
|
||||
#source a:hover * {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
#source a:hover pre {
|
||||
background: #FAFFCF !important;
|
||||
}
|
||||
#source em {
|
||||
font-style: normal;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
#source strong {
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
{{with .Error}}
|
||||
<div id="header" class="block">
|
||||
<h1>
|
||||
{{.Title}}
|
||||
</h1>
|
||||
<p>
|
||||
{{if .SourceType}}
|
||||
The {{.SourceType}} <strong>{{.Path}}</strong> does not compile: <strong>{{.Description}}</strong>
|
||||
{{else}}
|
||||
{{.Description}}
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
{{if .Path}}
|
||||
<div id="source" class="block">
|
||||
<h2>In {{.Path}}
|
||||
{{if .Line}}
|
||||
(around {{if .Line}}line {{.Line}}{{end}}{{if .Column}} column {{.Column}}{{end}})
|
||||
{{end}}
|
||||
</h2>
|
||||
{{range .ContextSource}}
|
||||
<div class="line {{if .IsError}}error{{end}}">
|
||||
<span class="lineNumber">{{.Line}}:</span>
|
||||
<pre>{{.Source}}</pre>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .MetaError}}
|
||||
<div id="source" class="block">
|
||||
<h2>Additionally, an error occurred while handling this error.</h2>
|
||||
<div class="line error">
|
||||
{{.MetaError}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Application error</title>
|
||||
</head>
|
||||
<body>
|
||||
{{if .DevMode}}
|
||||
{{template "errors/500-dev.html" .}}
|
||||
{{else}}
|
||||
<h1>Oops, an error occured</h1>
|
||||
<p>
|
||||
This exception has been logged.
|
||||
</p>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user