|
|
@ -7,4 +7,4 @@ env:
|
|||
- "PATH=/home/travis/gopath/bin:$PATH"
|
||||
|
||||
script:
|
||||
- go test -v
|
||||
- go test -v ./...
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at zack.scholl@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/briandowns/spinner"
|
||||
packages = ["."]
|
||||
revision = "5b875a9171af19dbde37e70a8fcbe2ebd7285e05"
|
||||
version = "1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cihub/seelog"
|
||||
packages = ["."]
|
||||
revision = "d2c6e5aa9fbfdd1c624e140287063c7730654115"
|
||||
version = "v2.6"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "02af3965c54e8cacf948b97fef38925c4120652c"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fatih/color"
|
||||
packages = ["."]
|
||||
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fatih/structs"
|
||||
packages = ["."]
|
||||
revision = "a720dfa8df582c51dee1b36feabb906bde1588bd"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/errwrap"
|
||||
packages = ["."]
|
||||
revision = "7554cd9344cec97297fa6649b055a8c98c2a1e55"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-multierror"
|
||||
packages = ["."]
|
||||
revision = "b7773ae218740a7be65057fc60b366a49b538a44"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mars9/crypt"
|
||||
packages = ["."]
|
||||
revision = "65899cf653ff022fe5c7fe504b439feed9e7e0fc"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/colorstring"
|
||||
packages = ["."]
|
||||
revision = "8631ce90f28644f54aeedcb3e389a85174e067d1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mr-tron/base58"
|
||||
packages = ["base58"]
|
||||
revision = "4df4dc6e86a912614d09719d10cad427b087cbfb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/schollz/bytetoword"
|
||||
packages = ["."]
|
||||
revision = "a75e6c9cd0e1fe6444905174c32d91aec9ce14f8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/schollz/mnemonicode"
|
||||
packages = ["."]
|
||||
revision = "15c9654387fad6d257aa28f9be57b9f124101955"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/schollz/peerdiscovery"
|
||||
packages = ["."]
|
||||
revision = "2c99137c9f8ff3597c2325c3be232612867652e3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/schollz/progressbar"
|
||||
packages = ["."]
|
||||
revision = "2283967e9a5af7a9eaddaaacb9f569314977fd03"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/schollz/tarinator-go"
|
||||
packages = ["."]
|
||||
revision = "0eab2c92d3ce5cb04535445b0825b2120c4acf3f"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/urfave/cli"
|
||||
packages = ["."]
|
||||
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
|
||||
version = "v1.20.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/yudai/gotty"
|
||||
packages = ["pkg/homedir"]
|
||||
revision = "a080c85cbc59226c94c6941ad8c395232d72d517"
|
||||
version = "v2.0.0-alpha.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/yudai/hcl"
|
||||
packages = [
|
||||
".",
|
||||
"hcl",
|
||||
"json"
|
||||
]
|
||||
revision = "5fa2393b3552119bf33a69adb1402a1160cba23d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"curve25519",
|
||||
"internal/subtle",
|
||||
"nacl/box",
|
||||
"nacl/secretbox",
|
||||
"pbkdf2",
|
||||
"poly1305",
|
||||
"salsa20/salsa",
|
||||
"scrypt",
|
||||
"ssh/terminal"
|
||||
]
|
||||
revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"internal/iana",
|
||||
"internal/socket",
|
||||
"ipv4"
|
||||
]
|
||||
revision = "afe8f62b1d6bbd81f31868121a50b06d8188e1f9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "c4afb3effaa53fd9a06ca61262dc7ce8df4c081b"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = ["transform"]
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "a89b8e32b8dbb85f236a17be149ba201f551f415791d1d6b95b203853fa7957c"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
90
Gopkg.toml
|
|
@ -1,90 +0,0 @@
|
|||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/cihub/seelog"
|
||||
version = "2.6.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/structs"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/mars9/crypt"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/schollz/mnemonicode"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/schollz/peerdiscovery"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/schollz/progressbar"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/schollz/tarinator-go"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.5"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/urfave/cli"
|
||||
version = "1.20.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/yudai/gotty"
|
||||
version = "2.0.0-alpha.3"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/yudai/hcl"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
version = "2.2.1"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
140
README.md
|
|
@ -1,9 +1,10 @@
|
|||
|
||||
<p align="center">
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/6550035/31846899-2b8a7034-b5cf-11e7-9643-afe552226c59.png"
|
||||
width="100%" border="0" alt="croc">
|
||||
<br>
|
||||
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-2.2.0-brightgreen.svg?style=flat-square" alt="Version"></a>
|
||||
<a href="https://github.com/schollz/croc/releases/latest"><img src="https://img.shields.io/badge/version-3.0.0-brightgreen.svg?style=flat-square" alt="Version"></a>
|
||||
<a href="https://saythanks.io/to/schollz"><img src="https://img.shields.io/badge/Say%20Thanks-!-yellow.svg?style=flat-square" alt="Go Report Card"></a>
|
||||
</p>
|
||||
|
||||
|
|
@ -12,13 +13,13 @@
|
|||
|
||||
*croc* allows any two computers to directly and securely transfer files and folders. When sending a file, *croc* generates a random code phrase which must be shared with the recipient so they can receive the file. The code phrase encrypts all data and metadata and also serves to authorize the connection between the two computers in a intermediary relay. The relay connects the TCP ports between the two computers and does not store any information (and all information passing through it is encrypted).
|
||||
|
||||
**New version released June 24th, 2018 - please upgrade if you are using the public relay.**
|
||||
**New version released July 3rd, 2018 - this version is in beta.**
|
||||
|
||||
I hear you asking, *Why another open-source peer-to-peer file transfer utilities?* [There](https://github.com/cowbell/sharedrop) [are](https://github.com/webtorrent/instant.io) [great](https://github.com/kern/filepizza) [tools](https://github.com/warner/magic-wormhole) [that](https://github.com/zerotier/toss) [already](https://github.com/ipfs/go-ipfs) [do](https://github.com/zerotier/toss) [this](https://github.com/nils-werner/zget). But, after review, [I found it was useful to make another](https://schollz.github.io/sending-a-file/). Namely, *croc* has no dependencies (just [download a binary and run](https://github.com/schollz/croc/releases/latest)), it works on any operating system, and its blazingly fast because it does parallel transfer over multiple TCP ports.
|
||||
|
||||
# Example
|
||||
|
||||
_These two gifs should run in sync if you force-reload (Ctl+F5)_
|
||||
<!-- _These two gifs should run in sync if you force-reload (Ctl+F5)_
|
||||
|
||||
**Sender:**
|
||||
|
||||
|
|
@ -26,23 +27,22 @@ _These two gifs should run in sync if you force-reload (Ctl+F5)_
|
|||
|
||||
**Receiver:**
|
||||
|
||||

|
||||
 -->
|
||||
|
||||
|
||||
**Sender:**
|
||||
|
||||
```
|
||||
$ croc -send some-file-or-folder
|
||||
$ croc send some-file-or-folder
|
||||
Sending 4.4 MB file named 'some-file-or-folder'
|
||||
Code is: cement-galaxy-alpha
|
||||
On the other computer please run
|
||||
|
||||
Your public key: ecad-bakery-cup-unlit-roam-fetid-arulo-updike
|
||||
Recipient public key: bike-cokery-casina-donut-field-farrow-mega-shine
|
||||
ok? (y/n): y
|
||||
croc cement-galaxy-alpha
|
||||
|
||||
Sending (->[1]63982)..
|
||||
89% |███████████████████████████████████ | [12s:1s]
|
||||
File sent (2.6 MB/s)
|
||||
Transfer complete.
|
||||
```
|
||||
|
||||
**Receiver:**
|
||||
|
|
@ -51,14 +51,11 @@ File sent (2.6 MB/s)
|
|||
$ croc
|
||||
Enter receive code: cement-galaxy-alpha
|
||||
Receiving file (4.4 MB) into: some-file-or-folder
|
||||
|
||||
Your public key: bike-cokery-casina-donut-field-farrow-mega-shine
|
||||
Recipient public key: ecad-bakery-cup-unlit-roam-fetid-arulo-updike
|
||||
ok? (y/n): y
|
||||
ok? (y/N): y
|
||||
|
||||
Receiving (<-[1]63975)..
|
||||
97% |██████████████████████████████████████ | [13s:0s]
|
||||
Received file written to some-file-or-folder (2.6 MB/s)
|
||||
Received file written to some-file-or-folder
|
||||
```
|
||||
|
||||
Note, by default, you don't need any arguments for receiving! This makes it possible for you to just double click the executable to run (nice for those of us that aren't computer wizards).
|
||||
|
|
@ -70,7 +67,7 @@ You can easily use *croc* in pipes when you need to send data through stdin or g
|
|||
**Sender:**
|
||||
|
||||
```
|
||||
$ cat some_file_or_folder | croc
|
||||
$ cat some_file_or_folder | croc send
|
||||
```
|
||||
|
||||
In this case *croc* will automatically use the stdin data and send and assign a filename like "croc-stdin-123456789".
|
||||
|
|
@ -78,7 +75,7 @@ In this case *croc* will automatically use the stdin data and send and assign a
|
|||
**Receiver:**
|
||||
|
||||
```
|
||||
$ croc --code code-phrase --yes --stdout | more
|
||||
$ croc --code code-phrase --yes --stdout receive
|
||||
```
|
||||
|
||||
Here the reciever specified the code (`--code`) so it will not be prompted, and also specified `--yes` so the file will be automatically accepted. The output goes to stdout when flagged with `--stdout`.
|
||||
|
|
@ -91,38 +88,109 @@ Here the reciever specified the code (`--code`) so it will not be prompted, and
|
|||
Or, you can [install Go](https://golang.org/dl/) and build from source with `go get github.com/schollz/croc`.
|
||||
|
||||
|
||||
# How does it work?
|
||||
|
||||
*croc* is similar to [magic-wormhole](https://github.com/warner/magic-wormhole#design) in spirit. Like *magic-wormhole*, *croc* generates a code phrase for you to share with your friend which allows secure end-to-end transferring of files and folders through a intermediary relay that connects the TCP ports between the two computers. The standard relay is on a public IP address (default `cowyo.com`), but before transmitting the file the two instances of *croc* send out UDP broadcasts to determine if they are both on the local network, and use a local relay instead of the cloud relay in the case that they are both local.
|
||||
# How does it work?
|
||||
|
||||
The code phrase allows the relay to match the receiver and the sender. Once matched, the relay shares the public keys so that the sender and recipient can further authenticate whether or not they have the right person. Once both sides verify and consent to the transfer, then the sender will encrypt the data using the recipient's public key, so that only they can decrypt the data. After a successful transfer, the public key is stored and next time it is not prompted and automatically trusted (TOFU).
|
||||
*croc* is similar to [magic-wormhole](https://github.com/warner/magic-wormhole#design) in spirit. Like *magic-wormhole*, *croc* generates a code phrase for you to share with your friend which allows secure end-to-end transferring of files and folders through a intermediary relay that connects the TCP ports between the two computers. Like *magic-wormhole*, security is enabled by performing password-authenticated key exchange (PAKE) with the weak code phrase to generate a session key on both machines without passing any private information between the two. The session key is then verified and used to encrypt the content with AES-256. If at any point the PAKE fails, an error will be reported and the file will not be transferred. More details on the PAKE transfer can be found at [github.com/schollz/pake](https://github.com/schollz/pake).
|
||||
|
||||
The transfer uses [Go channels](https://golang.org/doc/effective_go.html?h=chan#channels) and parallel connections to pipe all the data. After the transmission the channels are destroyed and all the connection and meta data information is wiped from the relay server. The encrypted file data never is stored on the relay.
|
||||
## Relay
|
||||
|
||||
**Security**
|
||||
|
||||
The first time you use croc you will generate a unique NaCl box keypair (which uses Curve25519, XSalsa20 and Poly1305) that is unique to your computer. This keypair is used to transfer the encryption key to the recipient, and guarantees that only the recipient can decrypt the encryption key to decrypt the file data. The encryption key is a cryptographically generated random 20 characters. The file data is encrypted using the encryption key with AES-256. This method guarantees that all the file data going over the wire is secure, and that the only person who can decrypt it is the recipient (i.e. a MITM attacker cannot decrypt it without the keypair on the recipient's computer).
|
||||
|
||||
The keypair also serves as a second method of authentication. After both the sender and recipient enter their code phrases, they will be able to see each other's public keys. If the public key of the other person does not match what they say it should be (i.e. an attacker is trying to use the same code phrase to get your file - possible though unlikely), then you can cancel the transfer.
|
||||
|
||||
On the receiver's computer, each piece of received encrypted data is written to a separate file. These files are concatenated and then decrypted. The hash of the decrypted file is then checked against the hash transmitted from the sender (part of the meta data block).
|
||||
|
||||
## Run your own relay
|
||||
|
||||
*croc* relies on a TCP relay to staple the parallel incoming and outgoing connections. The relay temporarily stores connection information and the encrypted meta information. The default uses a public relay at, `cowyo.com`, which has a 30-day uptime of 99.989% ([click here to check the current status of the public relay](https://stats.uptimerobot.com/lOwJYIgRm)).
|
||||
|
||||
You can also run your own relay, it is very easy. On your server, `your-server.com`, just run
|
||||
*croc* relies on a TCP relay to staple the parallel incoming and outgoing connections. The relay temporarily stores connection information and the encrypted meta information. The default uses a public relay at, `wss://croc3.schollz.com`. You can also run your own relay, it is very easy. On your server, `your-server.com`, just run
|
||||
|
||||
```
|
||||
$ croc -relay
|
||||
$ croc relay
|
||||
```
|
||||
|
||||
Now, when you use *croc* to send and receive you should add `-server your-server.com` to use your relay server. Make sure to open up TCP ports 27001-27009.
|
||||
Now, when you use *croc* to send and receive you should add `-server your-server.com` to use your relay server. Make sure to open up TCP ports (see `croc relay --help` for which ports to open).
|
||||
|
||||
# Contribute
|
||||
|
||||
I am awed by all the [great contributions](#acknowledgements) made! If you feel like contributing, in any way, by all means you can send an Issue, a PR, ask a question, or tweet me ([@yakczar](http://ctt.ec/Rq054)).
|
||||
|
||||
|
||||
|
||||
# Protocol
|
||||
|
||||
This is an outline of the protocol used here. The basic PAKE protocol is from [Dan Boneh and Victor Shoup's crypto book](https://crypto.stanford.edu/%7Edabo/cryptobook/BonehShoup_0_4.pdf) (pg 789, "PAKE2 protocol).
|
||||
|
||||

|
||||
|
||||
1. **Sender** requests new channel and receives empty channel from **Relay**, or obtains the channel they request (or an error if it is already occupied).
|
||||
2. **Sender** generates *u* using PAKE from secret *pw*.
|
||||
3. **Sender** sends *u* to **Relay** and the type of curve being used. Returns error if channel is already occupied by sender, otherwise it uses it.
|
||||
4. **Sender** communicates channel + secret *pw* to **Recipient** (human interaction).
|
||||
5. **Recipient** connects to channel and receives UUID.
|
||||
6. **Recipient** requests *u* from **Relay** using the channel. Returns error if it doesn't exist yet.
|
||||
7. **Recipient** generates *v*, session key *k_B*, and hashed session key *H(k_B)* using PAKE from secret *pw*.
|
||||
8. **Recipient** sends *v*, *H(H(k_B))* to **Relay**.
|
||||
9. **Sender** requests *v*, *H(H(k_B))* from **Relay**.
|
||||
10. **Sender** uses *v* to generate its session key *k_A* and *H(k_A)*, and checks *H(H(k_A))*==*H(H(k_B))*. **Sender** aborts here if it is incorrect.
|
||||
11. **Sender** gives the **Relay** authentication *H(k_A)*.
|
||||
12. **Recipient** requests *H(k_A)* from relay and checks against its own. If it doesn't match, then bail.
|
||||
13. **Sender** connects to **Relay** tcp ports and identifies itself using channel+UUID.
|
||||
14. **Sender** encrypts data with *k*.
|
||||
15. **Recipient** connects to **Relay** tcp ports and identifies itself using channel+UUID.
|
||||
16. **Relay** realizes it has both recipient and sender for the same channel so it staples their connections. Sets *stapled* to `true`.
|
||||
17. **Sender** asks **Relay** whether connections are stapled.
|
||||
18. **Sender** sends data over TCP.
|
||||
19. **Recipient** closes relay when finished. Anyone participating in the channel can close the relay at any time. Any of the routes except the first ones will return errors if stuff doesn't exist.
|
||||
|
||||
|
||||
### Conditions of state
|
||||
|
||||
The websocket implementation means that each client and relay follows their specific state machine conditions.
|
||||
|
||||
#### Sender
|
||||
|
||||
*Initialize*
|
||||
|
||||
- Requests to join.
|
||||
|
||||
*Does X not exist?*
|
||||
|
||||
- Generates X from pw.
|
||||
- Update relay with X.
|
||||
|
||||
*Is Y and Bcrypt(k_B) available?*
|
||||
|
||||
- Use *v* to generate its session key *k_A*.
|
||||
- Check that Bcrypt(k_B) comes from k_A. Abort here if it is incorrect.
|
||||
- Encrypts data using *k_A*.
|
||||
- Connect to TCP ports of Relay.
|
||||
- Update relay with *Bcrypt(k_A)*.
|
||||
|
||||
*Are ports stapled?*
|
||||
|
||||
- Send data over TCP
|
||||
|
||||
|
||||
#### Recipient
|
||||
|
||||
*Initialize*
|
||||
|
||||
- Request to join
|
||||
|
||||
*Is X available?*
|
||||
|
||||
- Generate *v*, session key *k_B*, and hashed session key *H(k_B)* using PAKE from secret *pw*.
|
||||
- Send the Relay *Bcrypt(k_B)*
|
||||
|
||||
*Is Bcrypt(k_A) available?*
|
||||
|
||||
- Verify that *Bcrypt(k_A)* comes from k_B
|
||||
- Connect to TCP ports of Relay and listen.
|
||||
- Once file is received, Send close signal to Relay.
|
||||
|
||||
|
||||
#### Relay
|
||||
|
||||
*Is there a listener for sender and recipient?*
|
||||
|
||||
- Staple connections.
|
||||
- Send out to all parties that connections are stapled.
|
||||
|
||||
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
|
@ -134,4 +202,4 @@ Thanks...
|
|||
- ...[@warner](https://github.com/warner) for the [idea](https://github.com/warner/magic-wormhole).
|
||||
- ...[@tscholl2](https://github.com/tscholl2) for the [encryption gists](https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28).
|
||||
- ...[@skorokithakis](https://github.com/skorokithakis) for [code on proxying two connections](https://www.stavros.io/posts/proxying-two-connections-go/).
|
||||
- ...for making pull requests [@Girbons](https://github.com/Girbons), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia), [@csleong98](https://github.com/csleong98), [@afotescu](https://github.com/afotescu), [@callmefever](https://github.com/callmefever), [@El-JojA](https://github.com/El-JojA), [@anatolyyyyyy](https://github.com/anatolyyyyyy), [@goggle](https://github.com/goggle), [@smileboywtu](https://github.com/smileboywtu)!
|
||||
- ...for making pull requests [@Girbons](https://github.com/Girbons), [@techtide](https://github.com/techtide), [@heymatthew](https://github.com/heymatthew), [@Lunsford94](https://github.com/Lunsford94), [@lummie](https://github.com/lummie), [@jesuiscamille](https://github.com/jesuiscamille), [@threefjord](https://github.com/threefjord), [@marcossegovia](https://github.com/marcossegovia), [@csleong98](https://github.com/csleong98), [@afotescu](https://github.com/afotescu), [@callmefever](https://github.com/callmefever), [@El-JojA](https://github.com/El-JojA), [@anatolyyyyyy](https://github.com/anatolyyyyyy), [@goggle](https://github.com/goggle), [@smileboywtu](https://github.com/smileboywtu)!
|
||||
1012
connect.go
|
|
@ -1,49 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncrypt(t *testing.T) {
|
||||
key := GetRandomName()
|
||||
encrypted, salt, iv := Encrypt([]byte("hello, world"), key)
|
||||
decrypted, err := Decrypt(encrypted, key, salt, iv)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if string(decrypted) != "hello, world" {
|
||||
t.Error("problem decrypting")
|
||||
}
|
||||
_, err = Decrypt(encrypted, "wrong passphrase", salt, iv)
|
||||
if err == nil {
|
||||
t.Error("should not work!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptFiles(t *testing.T) {
|
||||
key := GetRandomName()
|
||||
if err := ioutil.WriteFile("temp", []byte("hello, world!"), 0644); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := EncryptFile("temp", "temp.enc", key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := DecryptFile("temp.enc", "temp.dec", key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data, err := ioutil.ReadFile("temp.dec")
|
||||
if string(data) != "hello, world!" {
|
||||
t.Errorf("Got something weird: " + string(data))
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := DecryptFile("temp.enc", "temp.dec", key+"wrong password"); err == nil {
|
||||
t.Error("should throw error!")
|
||||
}
|
||||
os.Remove("temp.dec")
|
||||
os.Remove("temp.enc")
|
||||
os.Remove("temp")
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# .goreleaser.yml
|
||||
# Build customization
|
||||
builds:
|
||||
- binary: croc
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- 386
|
||||
goarm:
|
||||
- 6
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
archive:
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
darwin: OSX
|
||||
linux_arm: raspberry_pi
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
- goos: darwin
|
||||
format: zip
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
package keypair
|
||||
|
||||
import (
|
||||
crypto_rand "crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/mr-tron/base58/base58"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
Public string `json:"public"`
|
||||
Private string `json:"private,omitempty"`
|
||||
private *[32]byte
|
||||
public *[32]byte
|
||||
}
|
||||
|
||||
func (kp KeyPair) String() string {
|
||||
b, _ := json.Marshal(kp)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Load will load from a string
|
||||
func Load(keypairString string) (kp KeyPair, err error) {
|
||||
err = json.Unmarshal([]byte(keypairString), &kp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
kp, err = New(kp)
|
||||
return
|
||||
}
|
||||
|
||||
// New will generate a new key pair, or reload a keypair
|
||||
// from a public key or a public-private key pair.
|
||||
func New(kpLoad ...KeyPair) (kp KeyPair, err error) {
|
||||
kp = KeyPair{}
|
||||
if len(kpLoad) > 0 {
|
||||
kp.Public = kpLoad[0].Public
|
||||
kp.Private = kpLoad[0].Private
|
||||
} else {
|
||||
kp.Public, kp.Private, err = generateKeyPair()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
kp.public, err = keyToBytes(kp.Public)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(kp.Private) > 0 {
|
||||
kp.private, err = keyToBytes(kp.Private)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func generateKeyPair() (publicKey, privateKey string, err error) {
|
||||
publicKeyBytes, privateKeyBytes, err := box.GenerateKey(crypto_rand.Reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
publicKey = base58.FastBase58Encoding(publicKeyBytes[:])
|
||||
privateKey = base58.FastBase58Encoding(privateKeyBytes[:])
|
||||
return
|
||||
}
|
||||
|
||||
func keyToBytes(s string) (key *[32]byte, err error) {
|
||||
var keyBytes []byte
|
||||
keyBytes, err = base58.FastBase58Decoding(s)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
key = new([32]byte)
|
||||
copy(key[:], keyBytes[:32])
|
||||
return
|
||||
}
|
||||
|
||||
// Encrypt a message for a recipient
|
||||
func (kp KeyPair) Encrypt(msg []byte, recipientPublicKey string) (encrypted []byte, err error) {
|
||||
recipient, err := New(KeyPair{Public: recipientPublicKey})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encrypted, err = encryptWithKeyPair(msg, kp.private, recipient.public)
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt a message
|
||||
func (kp KeyPair) Decrypt(encrypted []byte, senderPublicKey string) (msg []byte, err error) {
|
||||
sender, err := New(KeyPair{Public: senderPublicKey})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg, err = decryptWithKeyPair(encrypted, sender.public, kp.private)
|
||||
return
|
||||
}
|
||||
|
||||
func encryptWithKeyPair(msg []byte, senderPrivateKey, recipientPublicKey *[32]byte) (encrypted []byte, err error) {
|
||||
// You must use a different nonce for each message you encrypt with the
|
||||
// same key. Since the nonce here is 192 bits long, a random value
|
||||
// provides a sufficiently small probability of repeats.
|
||||
var nonce [24]byte
|
||||
if _, err = io.ReadFull(crypto_rand.Reader, nonce[:]); err != nil {
|
||||
return
|
||||
}
|
||||
// This encrypts msg and appends the result to the nonce.
|
||||
encrypted = box.Seal(nonce[:], msg, &nonce, recipientPublicKey, senderPrivateKey)
|
||||
return
|
||||
}
|
||||
|
||||
func decryptWithKeyPair(enc []byte, senderPublicKey, recipientPrivateKey *[32]byte) (decrypted []byte, err error) {
|
||||
// The recipient can decrypt the message using their private key and the
|
||||
// sender's public key. When you decrypt, you must use the same nonce you
|
||||
// used to encrypt the message. One way to achieve this is to store the
|
||||
// nonce alongside the encrypted message. Above, we stored the nonce in the
|
||||
// first 24 bytes of the encrypted text.
|
||||
var decryptNonce [24]byte
|
||||
copy(decryptNonce[:], enc[:24])
|
||||
var ok bool
|
||||
decrypted, ok = box.Open(nil, enc[24:], &decryptNonce, senderPublicKey, recipientPrivateKey)
|
||||
if !ok {
|
||||
err = errors.New("keypair decryption failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// sliceForAppend takes a slice and a requested number of bytes. It returns a
|
||||
// slice with the contents of the given slice followed by that many bytes and a
|
||||
// second slice that aliases into it and contains only the extra bytes. If the
|
||||
// original slice has sufficient capacity then no allocation is performed.
|
||||
func sliceForAppend(in []byte, n int) (head, tail []byte) {
|
||||
if total := len(in) + n; cap(in) >= total {
|
||||
head = in[:total]
|
||||
} else {
|
||||
head = make([]byte, total)
|
||||
copy(head, in)
|
||||
}
|
||||
tail = head[len(in):]
|
||||
return
|
||||
}
|
||||
BIN
logo/1.gif
|
Before Width: | Height: | Size: 36 KiB |
BIN
logo/2.gif
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 86 KiB |
BIN
logo/sender.gif
|
Before Width: | Height: | Size: 50 KiB |
BIN
logo/sender2.gif
|
Before Width: | Height: | Size: 74 KiB |
448
main.go
|
|
@ -1,380 +1,130 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
"github.com/fatih/structs"
|
||||
croc "github.com/schollz/croc/src"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/yudai/gotty/pkg/homedir"
|
||||
"github.com/yudai/hcl"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const BUFFERSIZE = 1024
|
||||
|
||||
type AppConfig struct {
|
||||
Relay bool `yaml:"relay" flagName:"relay" flagSName:"r" flagDescribe:"Run as relay" default:"false"`
|
||||
Debug bool `yaml:"debug" flagName:"debug" flagSName:"d" flagDescribe:"Debug mode" default:"false"`
|
||||
Wait bool `yaml:"wait" flagName:"wait" flagSName:"w" flagDescribe:"Wait for code to be sent" default:"false"`
|
||||
PathSpec bool `yaml:"ask-save" flagName:"ask-save" flagSName:"q" flagDescribe:"Ask for path to save to" default:"false"`
|
||||
DontEncrypt bool `yaml:"no-encrypt" flagName:"no-encrypt" flagSName:"g" flagDescribe:"Turn off encryption" default:"false"`
|
||||
UseStdout bool `yaml:"stdout" flagName:"stdout" flagSName:"o" flagDescribe:"Use stdout" default:"false"`
|
||||
Yes bool `yaml:"yes" flagName:"yes" flagSName:"y" flagDescribe:"Automatically accept file" default:"false"`
|
||||
Local bool `yaml:"local" flagName:"local" flagSName:"lo" flagDescribe:"Use local relay when sending" default:"false"`
|
||||
NoLocal bool `yaml:"no-local" flagName:"no-local" flagSName:"nlo" flagDescribe:"Don't create local relay" default:"false"`
|
||||
Server string `yaml:"server" flagName:"server" flagSName:"l" flagDescribe:"Croc relay to use" default:"cowyo.com"`
|
||||
File string `yaml:"send" flagName:"send" flagSName:"s" flagDescribe:"File to send default:""`
|
||||
Path string `yaml:"save" flagName:"save" flagSName:"p" flagDescribe:"Path to save to" default:""`
|
||||
Code string `yaml:"code" flagName:"code" flagSName:"c" flagDescribe:"Use your own code phrase" default:""`
|
||||
Rate int `yaml:"rate" flagName:"rate" flagSName:"R" flagDescribe:"Throttle down to speed in kbps" default:"1000000"`
|
||||
NumberOfConnections int `yaml:"threads" flagName:"threads" flagSName:"n" flagDescribe:"Number of threads to use" default:"4"`
|
||||
}
|
||||
|
||||
var email string
|
||||
var author string
|
||||
var version string
|
||||
|
||||
func init() {
|
||||
|
||||
SetLogLevel("debug")
|
||||
}
|
||||
var cr *croc.Croc
|
||||
|
||||
func main() {
|
||||
defer log.Flush()
|
||||
app := cli.NewApp()
|
||||
app.Name = "croc"
|
||||
if version == "" {
|
||||
version = "dev"
|
||||
}
|
||||
|
||||
app.Version = version
|
||||
app.Author = author
|
||||
app.Email = email
|
||||
app.Usage = "send file by croc bridge"
|
||||
app.HideHelp = true
|
||||
|
||||
cli.AppHelpTemplate = helpTemplate
|
||||
|
||||
appOptions := &AppConfig{}
|
||||
if err := ApplyDefaultValues(appOptions); err != nil {
|
||||
exit(err, 1)
|
||||
}
|
||||
|
||||
cliFlags, flagMappings, err := GenerateFlags(appOptions)
|
||||
if err != nil {
|
||||
exit(err, 3)
|
||||
}
|
||||
|
||||
app.Flags = append(
|
||||
cliFlags,
|
||||
cli.StringFlag{
|
||||
Name: "config",
|
||||
Value: "~/.croc",
|
||||
Usage: "Config file path",
|
||||
EnvVar: "CROC_CONFIG",
|
||||
app.Compiled = time.Now()
|
||||
app.Usage = "easily and securely transfer stuff from one computer to another"
|
||||
app.UsageText = "croc allows any two computers to directly and securely transfer files"
|
||||
// app.ArgsUsage = "[args and such]"
|
||||
app.Commands = []cli.Command{
|
||||
cli.Command{
|
||||
Name: "send",
|
||||
Usage: "send a file",
|
||||
Description: "send a file over the relay",
|
||||
ArgsUsage: "[filename]",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{Name: "no-compress, o"},
|
||||
cli.BoolFlag{Name: "no-encrypt, e"},
|
||||
},
|
||||
HelpName: "croc send",
|
||||
Action: func(c *cli.Context) error {
|
||||
return send(c)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
app.Action = func(c *cli.Context) {
|
||||
|
||||
configFile := c.String("config")
|
||||
_, err := os.Stat(homedir.Expand(configFile))
|
||||
if configFile != "~/.croc" || !os.IsNotExist(err) {
|
||||
if err := ApplyConfigFileYaml(configFile, appOptions); err != nil {
|
||||
exit(err, 2)
|
||||
}
|
||||
}
|
||||
|
||||
ApplyFlags(cliFlags, flagMappings, c, appOptions)
|
||||
|
||||
if appOptions.Relay {
|
||||
fmt.Println("running relay on local address " + GetLocalIP())
|
||||
r := NewRelay(appOptions)
|
||||
r.Run()
|
||||
} else {
|
||||
c, err := NewConnection(appOptions)
|
||||
if err != nil {
|
||||
fmt.Printf("Error! Please submit the following error to https://github.com/schollz/croc/issues:\n\n'NewConnection: %s'\n\n", err.Error())
|
||||
return
|
||||
}
|
||||
err = c.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Error! Please submit the following error to https://github.com/schollz/croc/issues:\n\n'Run: %s'\n\n", err.Error())
|
||||
}
|
||||
}
|
||||
cli.Command{
|
||||
Name: "receive",
|
||||
Usage: "receive a file",
|
||||
Description: "receve a file over the relay",
|
||||
HelpName: "croc receive",
|
||||
Action: func(c *cli.Context) error {
|
||||
return receive(c)
|
||||
},
|
||||
},
|
||||
cli.Command{
|
||||
Name: "relay",
|
||||
Usage: "start a croc relay",
|
||||
Description: "the croc relay will handle websocket and TCP connections",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{Name: "tcp", Value: "27130,27131,27132,27133", Usage: "ports for the tcp connections"},
|
||||
cli.StringFlag{Name: "port", Value: "8130", Usage: "port that the websocket listens on"},
|
||||
cli.StringFlag{Name: "curve", Value: "siec", Usage: "specify elliptic curve to use (p224, p256, p384, p521, siec)"},
|
||||
},
|
||||
HelpName: "croc relay",
|
||||
Action: func(c *cli.Context) error {
|
||||
return relay(c)
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{Name: "relay", Value: "wss://croc3.schollz.com"},
|
||||
cli.StringFlag{Name: "code, c", Usage: "codephrase used to connect to relay"},
|
||||
cli.BoolFlag{Name: "no-local", Usage: "disable local mode"},
|
||||
cli.BoolFlag{Name: "local", Usage: "use only local mode"},
|
||||
cli.BoolFlag{Name: "debug", Usage: "increase verbosity (a lot)"},
|
||||
cli.BoolFlag{Name: "yes", Usage: "automatically agree to all prompts"},
|
||||
cli.BoolFlag{Name: "stdout", Usage: "redirect file to stdout"},
|
||||
}
|
||||
app.EnableBashCompletion = true
|
||||
app.HideHelp = false
|
||||
app.HideVersion = false
|
||||
app.BashComplete = func(c *cli.Context) {
|
||||
fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n")
|
||||
}
|
||||
app.Action = func(c *cli.Context) error {
|
||||
return cr.Receive(c.Args().First())
|
||||
}
|
||||
app.Before = func(c *cli.Context) error {
|
||||
cr = croc.Init()
|
||||
cr.AllowLocalDiscovery = true
|
||||
cr.WebsocketAddress = c.GlobalString("relay")
|
||||
cr.SetDebug(c.GlobalBool("debug"))
|
||||
cr.Yes = c.GlobalBool("yes")
|
||||
cr.Stdout = c.GlobalBool("stdout")
|
||||
cr.LocalOnly = c.GlobalBool("local")
|
||||
cr.NoLocal = c.GlobalBool("no-local")
|
||||
return nil
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func getInput(prompt string) string {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Fprintf(os.Stderr, "%s", prompt)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func exit(err error, code int) {
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func ApplyDefaultValues(struct_ interface{}) (err error) {
|
||||
o := structs.New(struct_)
|
||||
|
||||
for _, field := range o.Fields() {
|
||||
defaultValue := field.Tag("default")
|
||||
if defaultValue == "" {
|
||||
continue
|
||||
}
|
||||
var val interface{}
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
val = defaultValue
|
||||
case reflect.Bool:
|
||||
if defaultValue == "true" {
|
||||
val = true
|
||||
} else if defaultValue == "false" {
|
||||
val = false
|
||||
} else {
|
||||
return fmt.Errorf("invalid bool expression: %v, use true/false", defaultValue)
|
||||
}
|
||||
case reflect.Int:
|
||||
val, err = strconv.Atoi(defaultValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
val = field.Value()
|
||||
}
|
||||
field.Set(val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateFlags(options ...interface{}) (flags []cli.Flag, mappings map[string]string, err error) {
|
||||
mappings = make(map[string]string)
|
||||
|
||||
for _, struct_ := range options {
|
||||
o := structs.New(struct_)
|
||||
for _, field := range o.Fields() {
|
||||
flagName := field.Tag("flagName")
|
||||
if flagName == "" {
|
||||
continue
|
||||
}
|
||||
envName := "CROC_" + strings.ToUpper(strings.Join(strings.Split(flagName, "-"), "_"))
|
||||
mappings[flagName] = field.Name()
|
||||
|
||||
flagShortName := field.Tag("flagSName")
|
||||
if flagShortName != "" {
|
||||
flagName += ", " + flagShortName
|
||||
}
|
||||
|
||||
flagDescription := field.Tag("flagDescribe")
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
flags = append(flags, cli.StringFlag{
|
||||
Name: flagName,
|
||||
Value: field.Value().(string),
|
||||
Usage: flagDescription,
|
||||
EnvVar: envName,
|
||||
})
|
||||
case reflect.Bool:
|
||||
flags = append(flags, cli.BoolFlag{
|
||||
Name: flagName,
|
||||
Usage: flagDescription,
|
||||
EnvVar: envName,
|
||||
})
|
||||
case reflect.Int:
|
||||
flags = append(flags, cli.IntFlag{
|
||||
Name: flagName,
|
||||
Value: field.Value().(int),
|
||||
Usage: flagDescription,
|
||||
EnvVar: envName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ApplyFlags(
|
||||
flags []cli.Flag,
|
||||
mappingHint map[string]string,
|
||||
c *cli.Context,
|
||||
options ...interface{},
|
||||
) {
|
||||
objects := make([]*structs.Struct, len(options))
|
||||
for i, struct_ := range options {
|
||||
objects[i] = structs.New(struct_)
|
||||
}
|
||||
|
||||
for flagName, fieldName := range mappingHint {
|
||||
if !c.IsSet(flagName) {
|
||||
continue
|
||||
}
|
||||
var field *structs.Field
|
||||
var ok bool
|
||||
for _, o := range objects {
|
||||
field, ok = o.FieldOk(fieldName)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if field == nil {
|
||||
continue
|
||||
}
|
||||
var val interface{}
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
val = c.String(flagName)
|
||||
case reflect.Bool:
|
||||
val = c.Bool(flagName)
|
||||
case reflect.Int:
|
||||
val = c.Int(flagName)
|
||||
}
|
||||
field.Set(val)
|
||||
fmt.Printf("error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyConfigFile(filePath string, options ...interface{}) error {
|
||||
filePath = homedir.Expand(filePath)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return err
|
||||
func send(c *cli.Context) error {
|
||||
stat, _ := os.Stdin.Stat()
|
||||
var fname string
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
fname = "stdin"
|
||||
} else {
|
||||
fname = c.Args().First()
|
||||
}
|
||||
|
||||
fileString := []byte{}
|
||||
log.Debugf("Loading config file at: %s", filePath)
|
||||
fileString, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
if fname == "" {
|
||||
return errors.New("must specify file: croc send [filename]")
|
||||
}
|
||||
|
||||
for _, object := range options {
|
||||
if err := hcl.Decode(object, string(fileString)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
cr.UseCompression = !c.Bool("no-compress")
|
||||
cr.UseEncryption = !c.Bool("no-encrypt")
|
||||
return cr.Send(fname, c.GlobalString("code"))
|
||||
}
|
||||
|
||||
func ApplyConfigFileYaml(filePath string, options ...interface{}) error {
|
||||
filePath = homedir.Expand(filePath)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
fileString := []byte{}
|
||||
log.Debugf("Loading config file at: %s", filePath)
|
||||
fileString, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, object := range options {
|
||||
if err := yaml.Unmarshal(fileString, object); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
func receive(c *cli.Context) error {
|
||||
return cr.Receive(c.GlobalString("code"))
|
||||
}
|
||||
|
||||
func SaveConfigFileYaml(filePath string, options ...interface{}) error {
|
||||
filePath = homedir.Expand(filePath)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
for _, object := range options {
|
||||
if byteString, err := yaml.Marshal(object); err != nil {
|
||||
return err
|
||||
} else {
|
||||
fd.Write(byteString)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var helpTemplate = `
|
||||
,_
|
||||
>' )
|
||||
( ( \
|
||||
|| \
|
||||
/^^^^\ ||
|
||||
/^^\________/0 \ ||
|
||||
( ` + "`" + `~+++,,_||__,,++~^^^^^^^
|
||||
...V^V^V^V^V^V^\...............................
|
||||
|
||||
|
||||
NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} [options]
|
||||
|
||||
VERSION:
|
||||
{{.Version}}{{if or .Author .Email}}
|
||||
|
||||
AUTHOR:{{if .Author}}
|
||||
{{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}}
|
||||
{{.Email}}{{end}}{{end}}
|
||||
|
||||
OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
// SetLogLevel determines the log level
|
||||
func SetLogLevel(level string) (err error) {
|
||||
|
||||
// https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit
|
||||
// https://github.com/cihub/seelog/wiki/Log-levels
|
||||
appConfig := `
|
||||
<seelog minlevel="` + level + `">
|
||||
<outputs formatid="stdout">
|
||||
<filter levels="debug,trace">
|
||||
<console formatid="debug"/>
|
||||
</filter>
|
||||
<filter levels="info">
|
||||
<console formatid="info"/>
|
||||
</filter>
|
||||
<filter levels="critical,error">
|
||||
<console formatid="error"/>
|
||||
</filter>
|
||||
<filter levels="warn">
|
||||
<console formatid="warn"/>
|
||||
</filter>
|
||||
</outputs>
|
||||
<formats>
|
||||
<format id="stdout" format="%Date %Time [%LEVEL] %File %FuncShort:%Line %Msg %n" />
|
||||
<format id="debug" format="%Date %Time %EscM(37)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
|
||||
<format id="info" format="%EscM(36)[%LEVEL]%EscM(0) %Msg %n" />
|
||||
<format id="warn" format="%Date %Time %EscM(33)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
|
||||
<format id="error" format="%Date %Time %EscM(31)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
|
||||
</formats>
|
||||
</seelog>
|
||||
`
|
||||
logger, err := log.LoggerFromConfigAsBytes([]byte(appConfig))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.ReplaceLogger(logger)
|
||||
return
|
||||
func relay(c *cli.Context) error {
|
||||
cr.TcpPorts = strings.Split(c.String("tcp"), ",")
|
||||
cr.ServerPort = c.String("port")
|
||||
cr.CurveType = c.String("curve")
|
||||
return cr.Relay()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
package randomstring
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Adapted from https://elithrar.github.io/article/generating-secure-random-numbers-crypto-rand/
|
||||
|
||||
func init() {
|
||||
assertAvailablePRNG()
|
||||
}
|
||||
|
||||
func assertAvailablePRNG() {
|
||||
// Assert that a cryptographically secure PRNG is available.
|
||||
// Panic otherwise.
|
||||
buf := make([]byte, 1)
|
||||
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("crypto/rand is unavailable: Read() failed with %#v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateRandomBytes returns securely generated random bytes.
|
||||
// It will return an error if the system's secure random
|
||||
// number generator fails to function correctly, in which
|
||||
// case the caller should not continue.
|
||||
func GenerateRandomBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
// Note that err == nil only if we read len(b) bytes.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// GenerateRandomString returns a securely generated random string.
|
||||
// It will return an error if the system's secure random
|
||||
// number generator fails to function correctly, in which
|
||||
// case the caller should not continue.
|
||||
func GenerateRandomString(n int) (string, error) {
|
||||
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
|
||||
bytes, err := GenerateRandomBytes(n)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, b := range bytes {
|
||||
bytes[i] = letters[b%byte(len(letters))]
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// GenerateRandomStringURLSafe returns a URL-safe, base64 encoded
|
||||
// securely generated random string.
|
||||
// It will return an error if the system's secure random
|
||||
// number generator fails to function correctly, in which
|
||||
// case the caller should not continue.
|
||||
func GenerateRandomStringURLSafe(n int) (string, error) {
|
||||
b, err := GenerateRandomBytes(n)
|
||||
return base64.URLEncoding.EncodeToString(b), err
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package randomstring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
r, err := GenerateRandomString(20)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 20, len(r))
|
||||
fmt.Println(r)
|
||||
}
|
||||
489
relay.go
|
|
@ -1,489 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const MAX_NUMBER_THREADS = 8
|
||||
const CONNECTION_TIMEOUT = time.Hour
|
||||
|
||||
type connectionMap struct {
|
||||
receiver map[string]net.Conn
|
||||
sender map[string]net.Conn
|
||||
metadata map[string]string
|
||||
potentialReceivers map[string]struct{}
|
||||
rpublicKey map[string]string
|
||||
spublicKey map[string]string
|
||||
sconsent map[string]string
|
||||
passphrase map[string]string
|
||||
receiverReady map[string]bool
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *connectionMap) IsSenderConnected(key string) (found bool) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
_, found = c.sender[key]
|
||||
return
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
connections connectionMap
|
||||
Debug bool
|
||||
NumberOfConnections int
|
||||
}
|
||||
|
||||
func NewRelay(config *AppConfig) *Relay {
|
||||
r := &Relay{
|
||||
Debug: config.Debug,
|
||||
NumberOfConnections: MAX_NUMBER_THREADS,
|
||||
}
|
||||
|
||||
log.SetFormatter(&log.TextFormatter{})
|
||||
if r.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Relay) Run() {
|
||||
r.connections = connectionMap{}
|
||||
r.connections.Lock()
|
||||
r.connections.receiver = make(map[string]net.Conn)
|
||||
r.connections.sender = make(map[string]net.Conn)
|
||||
r.connections.metadata = make(map[string]string)
|
||||
r.connections.spublicKey = make(map[string]string)
|
||||
r.connections.rpublicKey = make(map[string]string)
|
||||
r.connections.passphrase = make(map[string]string)
|
||||
r.connections.sconsent = make(map[string]string)
|
||||
r.connections.potentialReceivers = make(map[string]struct{})
|
||||
r.connections.receiverReady = make(map[string]bool)
|
||||
r.connections.Unlock()
|
||||
r.runServer()
|
||||
}
|
||||
|
||||
func (r *Relay) runServer() {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "main",
|
||||
})
|
||||
logger.Debug("Initializing")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(r.NumberOfConnections)
|
||||
for id := 0; id < r.NumberOfConnections; id++ {
|
||||
go r.listenerThread(id, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (r *Relay) listenerThread(id int, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
if err := r.listener(id); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) listener(id int) (err error) {
|
||||
port := strconv.Itoa(27001 + id)
|
||||
logger := log.WithFields(log.Fields{
|
||||
"function": "listener:" + port,
|
||||
})
|
||||
server, err := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error listening on :"+port)
|
||||
}
|
||||
defer server.Close()
|
||||
logger.Debug("waiting for connections")
|
||||
//Spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "problem accepting connection")
|
||||
}
|
||||
logger.Debugf("Client %s connected", connection.RemoteAddr().String())
|
||||
go r.clientCommuncation(id, connection)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) clientCommuncation(id int, connection net.Conn) {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"id": id,
|
||||
"ip": connection.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
sendMessage("who?", connection)
|
||||
m := strings.Split(receiveMessage(connection), ".")
|
||||
if len(m) < 4 {
|
||||
logger.Debug("exiting, not enough information")
|
||||
sendMessage("not enough information", connection)
|
||||
return
|
||||
}
|
||||
connectionType, publicKey, codePhrase, metaData := m[0], m[1], m[2], m[3]
|
||||
logger.Debugf("got connection from %s", publicKey)
|
||||
key := codePhrase + "-" + strconv.Itoa(id)
|
||||
|
||||
switch connectionType {
|
||||
case "s": // sender connection
|
||||
startTime := time.Now()
|
||||
deleteAll := func() {
|
||||
r.connections.Lock()
|
||||
// close connections
|
||||
if _, ok := r.connections.sender[key]; ok {
|
||||
r.connections.sender[key].Close()
|
||||
}
|
||||
if _, ok := r.connections.receiver[key]; ok {
|
||||
r.connections.receiver[key].Close()
|
||||
}
|
||||
// delete connctions
|
||||
delete(r.connections.sender, key)
|
||||
delete(r.connections.receiver, key)
|
||||
delete(r.connections.metadata, key)
|
||||
delete(r.connections.potentialReceivers, key)
|
||||
delete(r.connections.spublicKey, key)
|
||||
delete(r.connections.rpublicKey, key)
|
||||
delete(r.connections.receiverReady, key)
|
||||
delete(r.connections.passphrase, key)
|
||||
r.connections.Unlock()
|
||||
logger.Debug("deleted sender and receiver")
|
||||
}
|
||||
defer deleteAll()
|
||||
|
||||
if r.connections.IsSenderConnected(key) {
|
||||
sendMessage("no", connection)
|
||||
return
|
||||
}
|
||||
|
||||
r.connections.Lock()
|
||||
r.connections.metadata[key] = metaData
|
||||
r.connections.sender[key] = connection
|
||||
r.connections.spublicKey[key] = publicKey
|
||||
r.connections.Unlock()
|
||||
// wait for receiver
|
||||
receiversAddress := ""
|
||||
receiversPublicKey := ""
|
||||
isTimeout := time.Duration(0)
|
||||
log.Debug("waiting for reciever for sender")
|
||||
for {
|
||||
if CONNECTION_TIMEOUT <= isTimeout {
|
||||
sendMessage("timeout", connection)
|
||||
return
|
||||
}
|
||||
r.connections.RLock()
|
||||
if _, ok := r.connections.receiver[key]; ok {
|
||||
receiversAddress = r.connections.receiver[key].RemoteAddr().String()
|
||||
}
|
||||
if _, ok := r.connections.rpublicKey[key]; ok {
|
||||
receiversPublicKey = r.connections.rpublicKey[key]
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
if receiversAddress != "" && receiversPublicKey != "" {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
isTimeout += 100 * time.Millisecond
|
||||
}
|
||||
logger.Debug("telling sender ok")
|
||||
sendMessage(receiversAddress, connection)
|
||||
sendMessage(receiversPublicKey, connection)
|
||||
|
||||
// TODO ASK FOR OKAY HERE TOO
|
||||
sconsent := receiveMessage(connection)
|
||||
r.connections.Lock()
|
||||
r.connections.sconsent[key] = sconsent
|
||||
r.connections.Unlock()
|
||||
logger.Debugf("got consent: %+v", sconsent)
|
||||
if sconsent != "ok" {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("waiting for encrypted passphrase")
|
||||
encryptedPassphrase := receiveMessage(connection)
|
||||
r.connections.Lock()
|
||||
r.connections.passphrase[key] = encryptedPassphrase
|
||||
r.connections.Unlock()
|
||||
|
||||
// wait for receiver ready
|
||||
startTime = time.Now()
|
||||
for {
|
||||
r.connections.RLock()
|
||||
if _, ok := r.connections.receiverReady[key]; ok {
|
||||
r.connections.RUnlock()
|
||||
break
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
if time.Since(startTime) > 5*time.Minute {
|
||||
return
|
||||
}
|
||||
}
|
||||
// go reciever ready tell sender to go
|
||||
sendMessage("go", connection)
|
||||
logger.Debug("preparing pipe")
|
||||
r.connections.Lock()
|
||||
con1 := r.connections.sender[key]
|
||||
con2 := r.connections.receiver[key]
|
||||
r.connections.Unlock()
|
||||
logger.Debug("piping connections")
|
||||
Pipe(con1, con2)
|
||||
logger.Debug("done piping")
|
||||
case "r", "c": // receiver
|
||||
startTime := time.Now()
|
||||
log.Debug("is receiver")
|
||||
|
||||
r.connections.RLock()
|
||||
_, foundReceiver := r.connections.potentialReceivers[key]
|
||||
r.connections.RUnlock()
|
||||
if foundReceiver {
|
||||
log.Debug("already have receiver")
|
||||
sendMessage("no", connection)
|
||||
return
|
||||
}
|
||||
|
||||
// add as a potential receiver
|
||||
logger.Debug("adding as potential reciever")
|
||||
r.connections.Lock()
|
||||
r.connections.potentialReceivers[key] = struct{}{}
|
||||
r.connections.rpublicKey[key] = publicKey
|
||||
r.connections.receiver[key] = connection
|
||||
r.connections.Unlock()
|
||||
// wait for sender's metadata
|
||||
sendersAddress := ""
|
||||
sendersPublicKey := ""
|
||||
startTime = time.Now()
|
||||
for {
|
||||
r.connections.RLock()
|
||||
// check if been deleted
|
||||
if _, ok := r.connections.potentialReceivers[key]; !ok {
|
||||
log.Debug("deleting and finishing")
|
||||
r.connections.RUnlock()
|
||||
return
|
||||
}
|
||||
if _, ok := r.connections.metadata[key]; ok {
|
||||
if _, ok2 := r.connections.sender[key]; ok2 {
|
||||
sendersAddress = r.connections.sender[key].RemoteAddr().String()
|
||||
logger.Debug("got sender meta data")
|
||||
}
|
||||
}
|
||||
if _, ok := r.connections.spublicKey[key]; ok {
|
||||
sendersPublicKey = r.connections.spublicKey[key]
|
||||
logger.Debugf("got sender public key: %s", sendersPublicKey)
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
if sendersAddress != "" && sendersPublicKey != "" {
|
||||
break
|
||||
}
|
||||
if connectionType == "c" {
|
||||
sendMessage("0-0-0-0.0.0.0", connection)
|
||||
// sender is not ready so delete connection
|
||||
r.connections.Lock()
|
||||
delete(r.connections.potentialReceivers, key)
|
||||
r.connections.Unlock()
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if time.Since(startTime) > 5*time.Minute {
|
||||
return
|
||||
}
|
||||
}
|
||||
// send meta data
|
||||
r.connections.RLock()
|
||||
sendMessage(r.connections.metadata[key]+"-"+sendersAddress, connection)
|
||||
sendMessage(sendersPublicKey, connection)
|
||||
r.connections.RUnlock()
|
||||
|
||||
// check for senders consent
|
||||
sendersConsent := ""
|
||||
startTime = time.Now()
|
||||
for {
|
||||
r.connections.RLock()
|
||||
// check if been deleted
|
||||
if _, ok := r.connections.potentialReceivers[key]; !ok {
|
||||
log.Debug("deleting and finishing")
|
||||
r.connections.RUnlock()
|
||||
return
|
||||
}
|
||||
if _, ok := r.connections.sconsent[key]; ok {
|
||||
sendersConsent = r.connections.sconsent[key]
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
if sendersConsent != "" {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if time.Since(startTime) > 5*time.Minute {
|
||||
return
|
||||
}
|
||||
}
|
||||
if sendersConsent != "ok" {
|
||||
// TODO: delete everything
|
||||
return
|
||||
}
|
||||
|
||||
// now get passphrase
|
||||
sendersPassphrase := ""
|
||||
startTime = time.Now()
|
||||
for {
|
||||
r.connections.RLock()
|
||||
// check if been deleted
|
||||
if _, ok := r.connections.potentialReceivers[key]; !ok {
|
||||
log.Debug("deleting and finishing")
|
||||
r.connections.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := r.connections.passphrase[key]; ok {
|
||||
sendersPassphrase = r.connections.passphrase[key]
|
||||
logger.Debugf("got sender passphrase: %s", sendersPassphrase)
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
if sendersPassphrase != "" {
|
||||
break
|
||||
}
|
||||
if time.Since(startTime) > 5*time.Minute {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// check for receiver's consent
|
||||
consent := receiveMessage(connection)
|
||||
logger.Debugf("consent: %s", consent)
|
||||
if consent == "ok" {
|
||||
logger.Debug("got consent")
|
||||
// wait for encrypted passphrase
|
||||
encryptedPassphrase := ""
|
||||
startTime = time.Now()
|
||||
for {
|
||||
r.connections.RLock()
|
||||
// check if been deleted
|
||||
if _, ok := r.connections.potentialReceivers[key]; !ok {
|
||||
log.Debug("deleting and finishing")
|
||||
r.connections.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := r.connections.passphrase[key]; ok {
|
||||
encryptedPassphrase = r.connections.passphrase[key]
|
||||
logger.Debugf("got passphrase: %s", r.connections.passphrase[key])
|
||||
}
|
||||
r.connections.RUnlock()
|
||||
if encryptedPassphrase != "" {
|
||||
break
|
||||
}
|
||||
if time.Since(startTime) > 5*time.Minute {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
sendMessage(encryptedPassphrase, connection)
|
||||
}
|
||||
receiveMessage(connection)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
r.connections.Lock()
|
||||
r.connections.receiverReady[key] = true
|
||||
r.connections.Unlock()
|
||||
default:
|
||||
logger.Debugf("Got unknown protocol: '%s'", connectionType)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(message string, connection net.Conn) {
|
||||
message = fillString(message, BUFFERSIZE)
|
||||
connection.Write([]byte(message))
|
||||
}
|
||||
|
||||
func receiveMessage(connection net.Conn) string {
|
||||
logger := log.WithFields(log.Fields{
|
||||
"func": "receiveMessage",
|
||||
"ip": connection.RemoteAddr().String(),
|
||||
})
|
||||
messageByte := make([]byte, BUFFERSIZE)
|
||||
err := connection.SetReadDeadline(time.Now().Add(60 * time.Minute))
|
||||
if err != nil {
|
||||
logger.Warn(err)
|
||||
}
|
||||
err = connection.SetDeadline(time.Now().Add(60 * time.Minute))
|
||||
if err != nil {
|
||||
logger.Warn(err)
|
||||
}
|
||||
err = connection.SetWriteDeadline(time.Now().Add(60 * time.Minute))
|
||||
if err != nil {
|
||||
logger.Warn(err)
|
||||
}
|
||||
_, err = connection.Read(messageByte)
|
||||
if err != nil {
|
||||
logger.Debug(err)
|
||||
logger.Debug("no response")
|
||||
return ""
|
||||
}
|
||||
return strings.TrimRight(string(messageByte), ":")
|
||||
}
|
||||
|
||||
func fillString(retunString string, toLength int) string {
|
||||
for {
|
||||
lengthString := len(retunString)
|
||||
if lengthString < toLength {
|
||||
retunString = retunString + ":"
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return retunString
|
||||
}
|
||||
|
||||
// chanFromConn creates a channel from a Conn object, and sends everything it
|
||||
// Read()s from the socket to the channel.
|
||||
func chanFromConn(conn net.Conn) chan []byte {
|
||||
c := make(chan []byte)
|
||||
|
||||
go func() {
|
||||
b := make([]byte, BUFFERSIZE)
|
||||
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
if n > 0 {
|
||||
res := make([]byte, n)
|
||||
// Copy the buffer so it doesn't get changed while read by the recipient.
|
||||
copy(res, b[:n])
|
||||
c <- res
|
||||
}
|
||||
if err != nil {
|
||||
c <- nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Pipe creates a full-duplex pipe between the two sockets and transfers data from one to the other.
|
||||
func Pipe(conn1 net.Conn, conn2 net.Conn) {
|
||||
chan1 := chanFromConn(conn1)
|
||||
chan2 := chanFromConn(conn2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case b1 := <-chan1:
|
||||
if b1 == nil {
|
||||
return
|
||||
}
|
||||
conn2.Write(b1)
|
||||
|
||||
case b2 := <-chan2:
|
||||
if b2 == nil {
|
||||
return
|
||||
}
|
||||
conn1.Write(b2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/schollz/peerdiscovery"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SetLogLevel("debug")
|
||||
}
|
||||
|
||||
// Relay initiates a relay
|
||||
func (c *Croc) Relay() error {
|
||||
// start relay
|
||||
go c.startRelay()
|
||||
|
||||
// start server
|
||||
return c.startServer()
|
||||
}
|
||||
|
||||
// Send will take an existing file or folder and send it through the croc relay
|
||||
func (c *Croc) Send(fname string, codePhrase string) (err error) {
|
||||
log.Debugf("sending %s with compression, encryption: (%v, %v)", fname, c.UseCompression, c.UseEncryption)
|
||||
// prepare code phrase
|
||||
defer c.cleanup()
|
||||
c.cs.Lock()
|
||||
c.cs.channel.codePhrase = codePhrase
|
||||
if len(codePhrase) == 0 {
|
||||
// generate code phrase
|
||||
codePhrase = getRandomName()
|
||||
}
|
||||
if len(codePhrase) < 4 {
|
||||
err = errors.New("code phrase must be more than 4 characters")
|
||||
c.cs.Unlock()
|
||||
return
|
||||
}
|
||||
c.cs.channel.codePhrase = codePhrase
|
||||
c.cs.channel.Channel = codePhrase[:3]
|
||||
c.cs.channel.passPhrase = codePhrase[3:]
|
||||
log.Debugf("codephrase: '%s'", codePhrase)
|
||||
log.Debugf("channel: '%s'", c.cs.channel.Channel)
|
||||
log.Debugf("passPhrase: '%s'", c.cs.channel.passPhrase)
|
||||
channel := c.cs.channel.Channel
|
||||
c.cs.Unlock()
|
||||
|
||||
// start peer discovery
|
||||
go func() {
|
||||
if c.NoLocal {
|
||||
return
|
||||
}
|
||||
log.Debug("listening for local croc relay...")
|
||||
go peerdiscovery.Discover(peerdiscovery.Settings{
|
||||
Limit: 1,
|
||||
TimeLimit: 600 * time.Second,
|
||||
Delay: 50 * time.Millisecond,
|
||||
Payload: []byte(codePhrase[:3]),
|
||||
})
|
||||
}()
|
||||
|
||||
if len(fname) == 0 {
|
||||
err = errors.New("must include filename")
|
||||
return
|
||||
}
|
||||
err = c.processFile(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// start relay for listening
|
||||
type runInfo struct {
|
||||
err error
|
||||
bothConnected bool
|
||||
}
|
||||
runClientError := make(chan runInfo, 2)
|
||||
go func() {
|
||||
if c.NoLocal {
|
||||
return
|
||||
}
|
||||
d := Init()
|
||||
d.ServerPort = "8140"
|
||||
d.TcpPorts = []string{"27140", "27141"}
|
||||
go d.startRelay()
|
||||
go d.startServer()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
ce := Init()
|
||||
ce.WebsocketAddress = "ws://127.0.0.1:8140"
|
||||
// copy over the information
|
||||
c.cs.Lock()
|
||||
ce.cs.Lock()
|
||||
ce.cs.channel.codePhrase = codePhrase
|
||||
ce.cs.channel.Channel = codePhrase[:3]
|
||||
ce.cs.channel.passPhrase = codePhrase[3:]
|
||||
ce.cs.channel.fileMetaData = c.cs.channel.fileMetaData
|
||||
ce.crocFile = c.crocFile
|
||||
ce.crocFileEncrypted = ce.crocFileEncrypted
|
||||
ce.isLocal = true
|
||||
ce.cs.Unlock()
|
||||
c.cs.Unlock()
|
||||
defer func() {
|
||||
// delete croc files
|
||||
ce.cleanup()
|
||||
}()
|
||||
var ri runInfo
|
||||
ri.err = ce.client(0, channel)
|
||||
ri.bothConnected = ce.bothConnected
|
||||
runClientError <- ri
|
||||
}()
|
||||
|
||||
// start main client
|
||||
go func() {
|
||||
if c.LocalOnly {
|
||||
return
|
||||
}
|
||||
var ri runInfo
|
||||
ri.err = c.client(0, channel)
|
||||
ri.bothConnected = c.bothConnected
|
||||
runClientError <- ri
|
||||
}()
|
||||
|
||||
var ri runInfo
|
||||
ri = <-runClientError
|
||||
if ri.bothConnected || c.LocalOnly || c.NoLocal {
|
||||
return ri.err
|
||||
}
|
||||
ri = <-runClientError
|
||||
return ri.err
|
||||
}
|
||||
|
||||
// Receive will receive something through the croc relay
|
||||
func (c *Croc) Receive(codePhrase string) (err error) {
|
||||
defer c.cleanup()
|
||||
if !c.NoLocal {
|
||||
// try to discovery codephrase and server through peer network
|
||||
discovered, errDiscover := peerdiscovery.Discover(peerdiscovery.Settings{
|
||||
Limit: 1,
|
||||
TimeLimit: 300 * time.Millisecond,
|
||||
Delay: 50 * time.Millisecond,
|
||||
Payload: []byte("checking"),
|
||||
})
|
||||
if errDiscover != nil {
|
||||
log.Debug(errDiscover)
|
||||
}
|
||||
if len(discovered) > 0 {
|
||||
log.Debugf("discovered %s on %s", discovered[0].Payload, discovered[0].Address)
|
||||
_, connectTimeout := net.DialTimeout("tcp", discovered[0].Address+":27140", 1*time.Second)
|
||||
if connectTimeout == nil {
|
||||
log.Debug("connected")
|
||||
c.WebsocketAddress = "ws://" + discovered[0].Address + ":8140"
|
||||
c.isLocal = true
|
||||
log.Debug(discovered[0].Address)
|
||||
// codePhrase = string(discovered[0].Payload)
|
||||
} else {
|
||||
log.Debug("but could not connect to ports")
|
||||
}
|
||||
} else {
|
||||
log.Debug("discovered no peers")
|
||||
}
|
||||
}
|
||||
|
||||
// prepare codephrase
|
||||
c.cs.Lock()
|
||||
if len(codePhrase) == 0 {
|
||||
// prompt codephrase
|
||||
codePhrase = promptCodePhrase()
|
||||
}
|
||||
if len(codePhrase) < 4 {
|
||||
err = errors.New("code phrase must be more than 4 characters")
|
||||
c.cs.Unlock()
|
||||
return
|
||||
}
|
||||
c.cs.channel.codePhrase = codePhrase
|
||||
c.cs.channel.Channel = codePhrase[:3]
|
||||
c.cs.channel.passPhrase = codePhrase[3:]
|
||||
log.Debugf("codephrase: '%s'", codePhrase)
|
||||
log.Debugf("channel: '%s'", c.cs.channel.Channel)
|
||||
log.Debugf("passPhrase: '%s'", c.cs.channel.passPhrase)
|
||||
channel := c.cs.channel.Channel
|
||||
c.cs.Unlock()
|
||||
|
||||
return c.client(1, channel)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (c *Croc) cleanup() {
|
||||
// erase all the croc files and their possible numbers
|
||||
for i := 0; i < 16; i++ {
|
||||
fname := c.crocFile + "." + strconv.Itoa(i)
|
||||
os.Remove(fname)
|
||||
}
|
||||
for i := 0; i < 16; i++ {
|
||||
fname := c.crocFileEncrypted + "." + strconv.Itoa(i)
|
||||
os.Remove(fname)
|
||||
}
|
||||
os.Remove(c.crocFile)
|
||||
os.Remove(c.crocFileEncrypted)
|
||||
c.cs.Lock()
|
||||
if c.cs.channel.fileMetaData.DeleteAfterSending {
|
||||
os.Remove(c.cs.channel.fileMetaData.Name)
|
||||
}
|
||||
defer c.cs.Unlock()
|
||||
}
|
||||
|
|
@ -0,0 +1,539 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/schollz/pake"
|
||||
"github.com/schollz/progressbar"
|
||||
)
|
||||
|
||||
var isPrinted bool
|
||||
|
||||
func (c *Croc) client(role int, channel string) (err error) {
|
||||
defer log.Flush()
|
||||
// initialize the channel data for this client
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
|
||||
// connect to the websocket
|
||||
u := url.URL{Scheme: strings.Split(c.WebsocketAddress, "://")[0], Host: strings.Split(c.WebsocketAddress, "://")[1], Path: "/"}
|
||||
log.Debugf("connecting to %s", u.String())
|
||||
ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
// don't return error if sender can't connect, so
|
||||
// that croc can be used locally without
|
||||
// an internet connection
|
||||
if role == 0 {
|
||||
log.Debugf("dial %s error: %s", c.WebsocketAddress, err.Error())
|
||||
err = nil
|
||||
} else {
|
||||
log.Error("dial:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
// add websocket to locked channel
|
||||
c.cs.Lock()
|
||||
c.cs.channel.ws = ws
|
||||
c.cs.Unlock()
|
||||
|
||||
// read in the messages and process them
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
var cd channelData
|
||||
err := ws.ReadJSON(&cd)
|
||||
if err != nil {
|
||||
log.Debugf("sender read error:", err)
|
||||
return
|
||||
}
|
||||
log.Debugf("recv: %s", cd.String2())
|
||||
err = c.processState(cd)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// initialize by joining as corresponding role
|
||||
// TODO:
|
||||
// allowing suggesting a channel
|
||||
p := channelData{
|
||||
Open: true,
|
||||
Role: role,
|
||||
Channel: channel,
|
||||
}
|
||||
log.Debugf("sending opening payload: %+v", p)
|
||||
c.cs.Lock()
|
||||
err = c.cs.channel.ws.WriteJSON(p)
|
||||
if err != nil {
|
||||
log.Errorf("problem opening: %s", err.Error())
|
||||
c.cs.Unlock()
|
||||
return
|
||||
}
|
||||
c.cs.Unlock()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-interrupt:
|
||||
// send Close signal to relay on interrupt
|
||||
log.Debugf("interrupt")
|
||||
c.cs.Lock()
|
||||
channel := c.cs.channel.Channel
|
||||
uuid := c.cs.channel.UUID
|
||||
// Cleanly close the connection by sending a close message and then
|
||||
// waiting (with timeout) for the server to close the connection.
|
||||
log.Debug("sending close signal")
|
||||
errWrite := ws.WriteJSON(channelData{
|
||||
Channel: channel,
|
||||
UUID: uuid,
|
||||
Close: true,
|
||||
})
|
||||
c.cs.Unlock()
|
||||
if errWrite != nil {
|
||||
log.Debugf("write close:", err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}(&wg)
|
||||
wg.Wait()
|
||||
|
||||
log.Debug("waiting for unlock")
|
||||
c.cs.Lock()
|
||||
if c.cs.channel.finishedHappy {
|
||||
log.Info("file recieved!")
|
||||
if c.cs.channel.Role == 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nTransfer complete.\n")
|
||||
} else {
|
||||
folderOrFile := "file"
|
||||
if c.cs.channel.fileMetaData.IsDir {
|
||||
folderOrFile = "folder"
|
||||
}
|
||||
// push to stdout if required
|
||||
if c.Stdout && !c.cs.channel.fileMetaData.IsDir {
|
||||
fmt.Fprintf(os.Stderr, "\nReceived %s written to %s", folderOrFile, "stdout")
|
||||
var bFile []byte
|
||||
bFile, err = ioutil.ReadFile(c.cs.channel.fileMetaData.Name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.Stdout.Write(bFile)
|
||||
os.Remove(c.cs.channel.fileMetaData.Name)
|
||||
} else {
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nReceived %s written to %s", folderOrFile, c.cs.channel.fileMetaData.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if c.cs.channel.Error != "" {
|
||||
err = errors.New(c.cs.channel.Error)
|
||||
} else {
|
||||
err = errors.New("one party canceled, file not transfered")
|
||||
}
|
||||
}
|
||||
c.cs.Unlock()
|
||||
log.Debug("returning")
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) processState(cd channelData) (err error) {
|
||||
c.cs.Lock()
|
||||
defer c.cs.Unlock()
|
||||
|
||||
// first check if there is relay reported error
|
||||
if cd.Error != "" {
|
||||
err = errors.New(cd.Error)
|
||||
return
|
||||
}
|
||||
// TODO:
|
||||
// check if the state is not aligned (i.e. have h(k) but no hh(k))
|
||||
// throw error if not aligned so it can exit
|
||||
|
||||
// if file received, then you are all done
|
||||
if cd.FileReceived {
|
||||
c.cs.channel.FileReceived = true
|
||||
c.cs.channel.finishedHappy = true
|
||||
log.Debug("file recieved!")
|
||||
log.Debug("sending close signal")
|
||||
c.cs.channel.Close = true
|
||||
c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, if ready to read, then set and return
|
||||
if cd.ReadyToRead {
|
||||
c.cs.channel.ReadyToRead = true
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, if transfer ready then send file
|
||||
if cd.TransferReady {
|
||||
c.cs.channel.TransferReady = true
|
||||
return
|
||||
}
|
||||
|
||||
// first update the channel data
|
||||
// initialize if has UUID
|
||||
if cd.UUID != "" {
|
||||
c.cs.channel.UUID = cd.UUID
|
||||
c.cs.channel.Channel = cd.Channel
|
||||
c.cs.channel.Role = cd.Role
|
||||
c.cs.channel.Curve = cd.Curve
|
||||
c.cs.channel.Pake, err = pake.Init([]byte(c.cs.channel.passPhrase), cd.Role, getCurve(cd.Curve))
|
||||
c.cs.channel.Update = true
|
||||
log.Debugf("updating channel")
|
||||
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
if errWrite != nil {
|
||||
log.Error(errWrite)
|
||||
}
|
||||
c.cs.channel.Update = false
|
||||
log.Debugf("initialized client state")
|
||||
return
|
||||
}
|
||||
// copy over the rest of the state
|
||||
c.cs.channel.Ports = cd.Ports
|
||||
c.cs.channel.EncryptedFileMetaData = cd.EncryptedFileMetaData
|
||||
c.cs.channel.Addresses = cd.Addresses
|
||||
if c.cs.channel.Role == 0 && c.isLocal {
|
||||
c.cs.channel.Addresses[0] = getLocalIP()
|
||||
log.Debugf("local IP: %s", c.cs.channel.Addresses[0])
|
||||
}
|
||||
c.bothConnected = cd.Addresses[0] != "" && cd.Addresses[1] != ""
|
||||
|
||||
// update the Pake
|
||||
if cd.Pake != nil && cd.Pake.Role != c.cs.channel.Role {
|
||||
if c.cs.channel.Pake.HkA == nil {
|
||||
log.Debugf("updating pake from %d", cd.Pake.Role)
|
||||
err = c.cs.channel.Pake.Update(cd.Pake.Bytes())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Debug("sending close signal")
|
||||
c.cs.channel.Close = true
|
||||
c.cs.channel.Error = err.Error()
|
||||
c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
return
|
||||
}
|
||||
c.cs.channel.Update = true
|
||||
log.Debugf("updating channel")
|
||||
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
if errWrite != nil {
|
||||
log.Error(errWrite)
|
||||
}
|
||||
c.cs.channel.Update = false
|
||||
}
|
||||
}
|
||||
if c.cs.channel.Role == 0 && c.cs.channel.Pake.IsVerified() && !c.cs.channel.notSentMetaData && !c.cs.channel.filesReady {
|
||||
go c.getFilesReady()
|
||||
c.cs.channel.filesReady = true
|
||||
}
|
||||
|
||||
// process the client state
|
||||
if c.cs.channel.Pake.IsVerified() && !c.cs.channel.isReady && c.cs.channel.EncryptedFileMetaData.Encrypted != nil {
|
||||
|
||||
// decrypt the meta data
|
||||
log.Debugf("encrypted meta data: %+v", c.cs.channel.EncryptedFileMetaData)
|
||||
var passphrase, metaDataBytes []byte
|
||||
passphrase, err = c.cs.channel.Pake.SessionKey()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
metaDataBytes, err = c.cs.channel.EncryptedFileMetaData.decrypt(passphrase)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(metaDataBytes, &c.cs.channel.fileMetaData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("meta data: %+v", c.cs.channel.fileMetaData)
|
||||
|
||||
// check if the user still wants to receive the file
|
||||
if c.cs.channel.Role == 1 {
|
||||
if !c.Yes {
|
||||
if !promptOkayToRecieve(c.cs.channel.fileMetaData) {
|
||||
log.Debug("sending close signal")
|
||||
c.cs.channel.Close = true
|
||||
c.cs.channel.Error = "refusing file"
|
||||
c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// spawn TCP connections
|
||||
c.cs.channel.isReady = true
|
||||
go c.spawnConnections(c.cs.channel.Role)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) spawnConnections(role int) (err error) {
|
||||
err = c.dialUp()
|
||||
if err == nil {
|
||||
if role == 1 {
|
||||
err = c.processReceivedFile()
|
||||
}
|
||||
} else {
|
||||
log.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) dialUp() (err error) {
|
||||
c.cs.Lock()
|
||||
ports := c.cs.channel.Ports
|
||||
channel := c.cs.channel.Channel
|
||||
uuid := c.cs.channel.UUID
|
||||
role := c.cs.channel.Role
|
||||
c.cs.Unlock()
|
||||
errorChan := make(chan error, len(ports))
|
||||
|
||||
if role == 1 {
|
||||
// generate a receive filename
|
||||
c.crocFileEncrypted = tempFileName("croc-received")
|
||||
}
|
||||
|
||||
for i, port := range ports {
|
||||
go func(channel, uuid, port string, i int, errorChan chan error) {
|
||||
if i == 0 {
|
||||
log.Debug("dialing up")
|
||||
}
|
||||
log.Debugf("connecting to %s", "localhost:"+port)
|
||||
address := strings.Split(strings.Split(c.WebsocketAddress, "://")[1], ":")[0]
|
||||
connection, err := net.Dial("tcp", address+":"+port)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
connection.SetReadDeadline(time.Now().Add(1 * time.Hour))
|
||||
connection.SetDeadline(time.Now().Add(1 * time.Hour))
|
||||
connection.SetWriteDeadline(time.Now().Add(1 * time.Hour))
|
||||
message, err := receiveMessage(connection)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
log.Debugf("relay says: %s", message)
|
||||
err = sendMessage(channel, connection)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
err = sendMessage(uuid, connection)
|
||||
if err != nil {
|
||||
errorChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// wait for transfer to be ready
|
||||
for {
|
||||
c.cs.RLock()
|
||||
ready := c.cs.channel.TransferReady
|
||||
if role == 0 {
|
||||
ready = ready && c.cs.channel.fileReady
|
||||
}
|
||||
c.cs.RUnlock()
|
||||
if ready {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if i == 0 {
|
||||
c.cs.Lock()
|
||||
c.bar = progressbar.NewOptions(c.cs.channel.fileMetaData.Size, progressbar.OptionSetWriter(os.Stderr))
|
||||
if role == 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nSending (->%s)...\n", c.cs.channel.Addresses[1])
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "\nReceiving (<-%s)...\n", c.cs.channel.Addresses[0])
|
||||
}
|
||||
c.cs.Unlock()
|
||||
}
|
||||
|
||||
if role == 0 {
|
||||
log.Debug("send file")
|
||||
for {
|
||||
c.cs.RLock()
|
||||
ready := c.cs.channel.ReadyToRead
|
||||
c.cs.RUnlock()
|
||||
if ready {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
log.Debug("sending file")
|
||||
filename := c.crocFileEncrypted + "." + strconv.Itoa(i)
|
||||
err = c.sendFile(filename, i, connection)
|
||||
} else {
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
c.cs.Lock()
|
||||
log.Debugf("updating channel with ready to read")
|
||||
c.cs.channel.Update = true
|
||||
c.cs.channel.ReadyToRead = true
|
||||
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
if errWrite != nil {
|
||||
log.Error(errWrite)
|
||||
}
|
||||
c.cs.channel.Update = false
|
||||
c.cs.Unlock()
|
||||
log.Debug("receive file")
|
||||
}()
|
||||
receiveFileName := c.crocFileEncrypted + "." + strconv.Itoa(i)
|
||||
log.Debugf("receiving file into %s", receiveFileName)
|
||||
err = c.receiveFile(receiveFileName, i, connection)
|
||||
}
|
||||
c.bar.Finish()
|
||||
errorChan <- err
|
||||
}(channel, uuid, port, i, errorChan)
|
||||
}
|
||||
|
||||
// collect errors
|
||||
for i := 0; i < len(ports); i++ {
|
||||
errOne := <-errorChan
|
||||
if errOne != nil {
|
||||
log.Warn(errOne)
|
||||
log.Debug("sending close signal")
|
||||
c.cs.channel.Close = true
|
||||
c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
}
|
||||
}
|
||||
log.Debug("leaving dialup")
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) receiveFile(filename string, id int, connection net.Conn) error {
|
||||
log.Debug("waiting for chunk size from sender")
|
||||
fileSizeBuffer := make([]byte, 10)
|
||||
connection.Read(fileSizeBuffer)
|
||||
fileDataString := strings.Trim(string(fileSizeBuffer), ":")
|
||||
fileSizeInt, _ := strconv.Atoi(fileDataString)
|
||||
chunkSize := int64(fileSizeInt)
|
||||
log.Debugf("chunk size: %d", chunkSize)
|
||||
if chunkSize == 0 {
|
||||
log.Debug(fileSizeBuffer)
|
||||
return errors.New("chunk size is empty!")
|
||||
}
|
||||
|
||||
os.Remove(filename)
|
||||
log.Debug("making " + filename)
|
||||
newFile, err := os.Create(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer newFile.Close()
|
||||
|
||||
log.Debug(id, "waiting for file")
|
||||
var receivedBytes int64
|
||||
receivedFirstBytes := false
|
||||
for {
|
||||
if (chunkSize - receivedBytes) < bufferSize {
|
||||
log.Debugf("%d at the end: %d < %d", id, (chunkSize - receivedBytes), bufferSize)
|
||||
io.CopyN(newFile, connection, (chunkSize - receivedBytes))
|
||||
// Empty the remaining bytes that we don't need from the network buffer
|
||||
if (receivedBytes+bufferSize)-chunkSize < bufferSize {
|
||||
log.Debug(id, "empty remaining bytes from network buffer")
|
||||
connection.Read(make([]byte, (receivedBytes+bufferSize)-chunkSize))
|
||||
}
|
||||
break
|
||||
}
|
||||
written, _ := io.CopyN(newFile, connection, bufferSize)
|
||||
receivedBytes += written
|
||||
c.bar.Add(int(written))
|
||||
|
||||
if !receivedFirstBytes {
|
||||
receivedFirstBytes = true
|
||||
log.Debug(id, "Received first bytes!")
|
||||
}
|
||||
}
|
||||
log.Debug(id, "received file")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Croc) sendFile(filename string, id int, connection net.Conn) error {
|
||||
|
||||
// open encrypted file chunk, if it exists
|
||||
log.Debug("opening encrypted file chunk: " + filename)
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// determine and send the file size to client
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
log.Debugf("sending chunk size: %d", fi.Size())
|
||||
log.Debug(connection.RemoteAddr())
|
||||
_, err = connection.Write([]byte(fillString(strconv.FormatInt(int64(fi.Size()), 10), 10)))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Problem sending chunk data: ")
|
||||
}
|
||||
|
||||
// rate limit the bandwidth
|
||||
log.Debug("determining rate limiting")
|
||||
rate := 10000
|
||||
throttle := time.NewTicker(time.Second / time.Duration(rate))
|
||||
log.Debugf("rate: %+v", rate)
|
||||
defer throttle.Stop()
|
||||
|
||||
// send the file
|
||||
sendBuffer := make([]byte, bufferSize)
|
||||
totalBytesSent := 0
|
||||
for range throttle.C {
|
||||
_, err := file.Read(sendBuffer)
|
||||
written, _ := connection.Write(sendBuffer)
|
||||
totalBytesSent += written
|
||||
c.bar.Add(written)
|
||||
// if errWrite != nil {
|
||||
// errWrite = errors.Wrap(errWrite, "problem writing to connection")
|
||||
// return errWrite
|
||||
// }
|
||||
if err == io.EOF {
|
||||
//End of file reached, break out of for loop
|
||||
log.Debug("EOF")
|
||||
err = nil // not really an error
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Debug("file is sent")
|
||||
log.Debug("removing piece")
|
||||
os.Remove(filename)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package croc
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
|
|
@ -6,17 +6,15 @@ import (
|
|||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
mathrand "math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
"github.com/mars9/crypt"
|
||||
"github.com/schollz/mnemonicode"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
|
|
@ -24,17 +22,25 @@ func init() {
|
|||
mathrand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
|
||||
func GetRandomName() string {
|
||||
func getRandomName() string {
|
||||
result := []string{}
|
||||
bs := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(bs, mathrand.Uint32())
|
||||
rand.Read(bs)
|
||||
result = mnemonicode.EncodeWordList(result, bs)
|
||||
return strings.Join(result, "-")
|
||||
}
|
||||
|
||||
func Encrypt(plaintext []byte, passphrase string, dontencrypt ...bool) (encrypted []byte, salt string, iv string) {
|
||||
type encryption struct {
|
||||
Encrypted, Salt, IV []byte
|
||||
}
|
||||
|
||||
func encrypt(plaintext []byte, passphrase []byte, dontencrypt ...bool) encryption {
|
||||
if len(dontencrypt) > 0 && dontencrypt[0] {
|
||||
return plaintext, "salt", "iv"
|
||||
return encryption{
|
||||
Encrypted: plaintext,
|
||||
Salt: []byte("salt"),
|
||||
IV: []byte("iv"),
|
||||
}
|
||||
}
|
||||
key, saltBytes := deriveKey(passphrase, nil)
|
||||
ivBytes := make([]byte, 12)
|
||||
|
|
@ -43,26 +49,26 @@ func Encrypt(plaintext []byte, passphrase string, dontencrypt ...bool) (encrypte
|
|||
rand.Read(ivBytes)
|
||||
b, _ := aes.NewCipher(key)
|
||||
aesgcm, _ := cipher.NewGCM(b)
|
||||
encrypted = aesgcm.Seal(nil, ivBytes, plaintext, nil)
|
||||
salt = hex.EncodeToString(saltBytes)
|
||||
iv = hex.EncodeToString(ivBytes)
|
||||
return
|
||||
encrypted := aesgcm.Seal(nil, ivBytes, plaintext, nil)
|
||||
return encryption{
|
||||
Encrypted: encrypted,
|
||||
Salt: saltBytes,
|
||||
IV: ivBytes,
|
||||
}
|
||||
}
|
||||
|
||||
func Decrypt(data []byte, passphrase string, salt string, iv string, dontencrypt ...bool) (plaintext []byte, err error) {
|
||||
func (e encryption) decrypt(passphrase []byte, dontencrypt ...bool) (plaintext []byte, err error) {
|
||||
if len(dontencrypt) > 0 && dontencrypt[0] {
|
||||
return data, nil
|
||||
return e.Encrypted, nil
|
||||
}
|
||||
saltBytes, _ := hex.DecodeString(salt)
|
||||
ivBytes, _ := hex.DecodeString(iv)
|
||||
key, _ := deriveKey(passphrase, saltBytes)
|
||||
key, _ := deriveKey(passphrase, e.Salt)
|
||||
b, _ := aes.NewCipher(key)
|
||||
aesgcm, _ := cipher.NewGCM(b)
|
||||
plaintext, err = aesgcm.Open(nil, ivBytes, data, nil)
|
||||
plaintext, err = aesgcm.Open(nil, e.IV, e.Encrypted, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func deriveKey(passphrase string, salt []byte) ([]byte, []byte) {
|
||||
func deriveKey(passphrase []byte, salt []byte) ([]byte, []byte) {
|
||||
if salt == nil {
|
||||
salt = make([]byte, 8)
|
||||
// http://www.ietf.org/rfc/rfc2898.txt
|
||||
|
|
@ -72,24 +78,24 @@ func deriveKey(passphrase string, salt []byte) ([]byte, []byte) {
|
|||
return pbkdf2.Key([]byte(passphrase), salt, 1000, 32, sha256.New), salt
|
||||
}
|
||||
|
||||
func Hash(data string) string {
|
||||
return HashBytes([]byte(data))
|
||||
func hash(data string) string {
|
||||
return hashBytes([]byte(data))
|
||||
}
|
||||
|
||||
func HashBytes(data []byte) string {
|
||||
func hashBytes(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
||||
func EncryptFile(inputFilename string, outputFilename string, password string) error {
|
||||
func encryptFile(inputFilename string, outputFilename string, password []byte) error {
|
||||
return cryptFile(inputFilename, outputFilename, password, true)
|
||||
}
|
||||
|
||||
func DecryptFile(inputFilename string, outputFilename string, password string) error {
|
||||
func decryptFile(inputFilename string, outputFilename string, password []byte) error {
|
||||
return cryptFile(inputFilename, outputFilename, password, false)
|
||||
}
|
||||
|
||||
func cryptFile(inputFilename string, outputFilename string, password string, encrypt bool) error {
|
||||
func cryptFile(inputFilename string, outputFilename string, password []byte, encrypt bool) error {
|
||||
in, err := os.Open(inputFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -110,7 +116,7 @@ func cryptFile(inputFilename string, outputFilename string, password string, enc
|
|||
c := &crypt.Crypter{
|
||||
HashFunc: sha1.New,
|
||||
HashSize: sha1.Size,
|
||||
Key: crypt.NewPbkdf2Key([]byte(password), 32),
|
||||
Key: crypt.NewPbkdf2Key(password, 32),
|
||||
}
|
||||
if encrypt {
|
||||
if err := c.Encrypt(out, in); err != nil {
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package croc
|
||||
|
||||
// func TestEncrypt(t *testing.T) {
|
||||
// key := getRandomName()
|
||||
// encrypted, salt, iv := encrypt([]byte("hello, world"), key)
|
||||
// decrypted, err := decrypt(encrypted, key, salt, iv)
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
// if string(decrypted) != "hello, world" {
|
||||
// t.Error("problem decrypting")
|
||||
// }
|
||||
// _, err = decrypt(encrypted, "wrong passphrase", salt, iv)
|
||||
// if err == nil {
|
||||
// t.Error("should not work!")
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestEncryptFiles(t *testing.T) {
|
||||
// key := getRandomName()
|
||||
// if err := ioutil.WriteFile("temp", []byte("hello, world!"), 0644); err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
// if err := encryptFile("temp", "temp.enc", key); err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
// if err := decryptFile("temp.enc", "temp.dec", key); err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
// data, err := ioutil.ReadFile("temp.dec")
|
||||
// if string(data) != "hello, world!" {
|
||||
// t.Errorf("Got something weird: " + string(data))
|
||||
// }
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
// if err := decryptFile("temp.enc", "temp.dec", key+"wrong password"); err == nil {
|
||||
// t.Error("should throw error!")
|
||||
// }
|
||||
// os.Remove("temp.dec")
|
||||
// os.Remove("temp.enc")
|
||||
// os.Remove("temp")
|
||||
// }
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Croc) processFile(src string) (err error) {
|
||||
log.Debug("processing file")
|
||||
defer func() {
|
||||
log.Debug("finished processing file")
|
||||
}()
|
||||
fd := FileMetaData{}
|
||||
|
||||
// pathToFile and filename are the files that should be used internally
|
||||
var pathToFile, filename string
|
||||
// first check if it is stdin
|
||||
if src == "stdin" {
|
||||
var f *os.File
|
||||
f, err = ioutil.TempFile(".", "croc-stdin-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(f, os.Stdin)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pathToFile = "."
|
||||
filename = f.Name()
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// fd.Name is what the user will see
|
||||
fd.Name = filename
|
||||
fd.DeleteAfterSending = true
|
||||
} else {
|
||||
if !exists(src) {
|
||||
err = errors.Errorf("file/folder '%s' does not exist", src)
|
||||
return
|
||||
}
|
||||
pathToFile, filename = filepath.Split(filepath.Clean(src))
|
||||
fd.Name = filename
|
||||
}
|
||||
|
||||
// check wether the file is a dir
|
||||
info, err := os.Stat(path.Join(pathToFile, filename))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
fd.IsDir = info.Mode().IsDir()
|
||||
|
||||
// zip file
|
||||
c.crocFile, err = zipFile(path.Join(pathToFile, filename), c.UseCompression)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
fd.IsCompressed = c.UseCompression
|
||||
fd.IsEncrypted = c.UseEncryption
|
||||
|
||||
fd.Hash, err = hashFile(c.crocFile)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
fd.Size, err = fileSize(c.crocFile)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not determine filesize")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.cs.Lock()
|
||||
defer c.cs.Unlock()
|
||||
c.cs.channel.fileMetaData = fd
|
||||
go showIntro(c.cs.channel.codePhrase, fd)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) getFilesReady() (err error) {
|
||||
log.Debug("getting files ready")
|
||||
defer func() {
|
||||
log.Debug("files ready")
|
||||
}()
|
||||
c.cs.Lock()
|
||||
defer c.cs.Unlock()
|
||||
c.cs.channel.notSentMetaData = true
|
||||
// send metadata
|
||||
|
||||
// wait until data is ready
|
||||
for {
|
||||
if c.cs.channel.fileMetaData.Name != "" {
|
||||
break
|
||||
}
|
||||
c.cs.Unlock()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
c.cs.Lock()
|
||||
}
|
||||
|
||||
// get passphrase
|
||||
var passphrase []byte
|
||||
passphrase, err = c.cs.channel.Pake.SessionKey()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if c.UseEncryption {
|
||||
// encrypt file data
|
||||
c.crocFileEncrypted = tempFileName("croc-encrypted")
|
||||
err = encryptFile(c.crocFile, c.crocFileEncrypted, passphrase)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
// remove the unencrypted versoin
|
||||
if err = os.Remove(c.crocFile); err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
c.cs.channel.fileMetaData.IsEncrypted = true
|
||||
} else {
|
||||
c.crocFileEncrypted = c.crocFile
|
||||
}
|
||||
// split into pieces to send
|
||||
log.Debugf("splitting %s", c.crocFileEncrypted)
|
||||
if err = splitFile(c.crocFileEncrypted, len(c.cs.channel.Ports)); err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
// remove the file now since we still have pieces
|
||||
if err = os.Remove(c.crocFileEncrypted); err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// encrypt meta data
|
||||
var metaDataBytes []byte
|
||||
metaDataBytes, err = json.Marshal(c.cs.channel.fileMetaData)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
c.cs.channel.EncryptedFileMetaData = encrypt(metaDataBytes, passphrase)
|
||||
|
||||
c.cs.channel.Update = true
|
||||
log.Debugf("updating channel with file information")
|
||||
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
if errWrite != nil {
|
||||
log.Error(errWrite)
|
||||
}
|
||||
c.cs.channel.Update = false
|
||||
go func() {
|
||||
c.cs.Lock()
|
||||
c.cs.channel.fileReady = true
|
||||
c.cs.Unlock()
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) processReceivedFile() (err error) {
|
||||
// cat the file received
|
||||
c.cs.Lock()
|
||||
defer c.cs.Unlock()
|
||||
c.cs.channel.FileReceived = true
|
||||
defer func() {
|
||||
c.cs.channel.Update = true
|
||||
errWrite := c.cs.channel.ws.WriteJSON(c.cs.channel)
|
||||
if errWrite != nil {
|
||||
log.Error(errWrite)
|
||||
return
|
||||
}
|
||||
c.cs.channel.Update = false
|
||||
}()
|
||||
|
||||
filesToCat := make([]string, len(c.cs.channel.Ports))
|
||||
for i := range c.cs.channel.Ports {
|
||||
filesToCat[i] = c.crocFileEncrypted + "." + strconv.Itoa(i)
|
||||
log.Debugf("going to cat file %s", filesToCat[i])
|
||||
}
|
||||
|
||||
// defer os.Remove(c.crocFile)
|
||||
log.Debugf("catting file into %s", c.crocFile)
|
||||
err = catFiles(filesToCat, c.crocFileEncrypted, true)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// unencrypt
|
||||
c.crocFile = tempFileName("croc-unencrypted")
|
||||
var passphrase []byte
|
||||
passphrase, err = c.cs.channel.Pake.SessionKey()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
// decrypt if was encrypted on the other side
|
||||
if c.cs.channel.fileMetaData.IsEncrypted {
|
||||
err = decryptFile(c.crocFileEncrypted, c.crocFile, passphrase)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
os.Remove(c.crocFileEncrypted)
|
||||
} else {
|
||||
c.crocFile = c.crocFileEncrypted
|
||||
}
|
||||
|
||||
// check hash
|
||||
log.Debug("checking hash")
|
||||
var hashString string
|
||||
hashString, err = hashFile(c.crocFile)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if hashString == c.cs.channel.fileMetaData.Hash {
|
||||
log.Debug("hashes match")
|
||||
} else {
|
||||
err = errors.Errorf("hashes do not match, %s != %s", c.cs.channel.fileMetaData.Hash, hashString)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// unzip file
|
||||
err = unzipFile(c.crocFile, ".")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
os.Remove(c.crocFile)
|
||||
c.cs.channel.finishedHappy = true
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
log "github.com/cihub/seelog"
|
||||
)
|
||||
|
||||
// SetLogLevel determines the log level
|
||||
func SetLogLevel(level string) (err error) {
|
||||
|
||||
// https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit
|
||||
// https://github.com/cihub/seelog/wiki/Log-levels
|
||||
appConfig := `
|
||||
<seelog minlevel="` + level + `">
|
||||
<outputs formatid="stdout">
|
||||
<filter levels="debug,trace">
|
||||
<console formatid="debug"/>
|
||||
</filter>
|
||||
<filter levels="info">
|
||||
<console formatid="info"/>
|
||||
</filter>
|
||||
<filter levels="critical,error">
|
||||
<console formatid="error"/>
|
||||
</filter>
|
||||
<filter levels="warn">
|
||||
<console formatid="warn"/>
|
||||
</filter>
|
||||
</outputs>
|
||||
<formats>
|
||||
<format id="stdout" format="%Date %Time [%LEVEL] %File %FuncShort:%Line %Msg %n" />
|
||||
<format id="debug" format="%Date %Time %EscM(37)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
|
||||
<format id="info" format="%Date %Time %EscM(36)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
|
||||
<format id="warn" format="%Date %Time %EscM(33)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
|
||||
<format id="error" format="%Date %Time %EscM(31)[%LEVEL]%EscM(0) %File %FuncShort:%Line %Msg %n" />
|
||||
</formats>
|
||||
</seelog>
|
||||
`
|
||||
logger, err := log.LoggerFromConfigAsBytes([]byte(appConfig))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.ReplaceLogger(logger)
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/schollz/pake"
|
||||
"github.com/schollz/progressbar"
|
||||
)
|
||||
|
||||
const (
|
||||
// maximum buffer size for initial TCP communication
|
||||
bufferSize = 1024
|
||||
)
|
||||
|
||||
type Croc struct {
|
||||
// Options for all
|
||||
Debug bool
|
||||
|
||||
// Options for relay
|
||||
ServerPort string
|
||||
CurveType string
|
||||
|
||||
// Options for connecting to server
|
||||
TcpPorts []string
|
||||
WebsocketAddress string
|
||||
Timeout time.Duration
|
||||
LocalOnly bool
|
||||
NoLocal bool
|
||||
|
||||
// Options for file transfering
|
||||
UseEncryption bool
|
||||
UseCompression bool
|
||||
AllowLocalDiscovery bool
|
||||
Yes bool
|
||||
Stdout bool
|
||||
|
||||
// private variables
|
||||
|
||||
// localIP address
|
||||
localIP string
|
||||
// is using local relay
|
||||
isLocal bool
|
||||
|
||||
// rs relay state is only for the relay
|
||||
rs relayState
|
||||
|
||||
// cs keeps the client state
|
||||
cs clientState
|
||||
bar *progressbar.ProgressBar
|
||||
|
||||
// crocFile is the name of the file that is prepared to sent
|
||||
crocFile string
|
||||
// crocFileEncrypted is the name of the encrypted file
|
||||
crocFileEncrypted string
|
||||
// bothConnected
|
||||
bothConnected bool
|
||||
}
|
||||
|
||||
// Init will initialize the croc relay
|
||||
func Init() (c *Croc) {
|
||||
c = new(Croc)
|
||||
c.TcpPorts = []string{"27030", "27031", "27032", "27033"}
|
||||
c.Timeout = 10 * time.Minute
|
||||
c.UseEncryption = true
|
||||
c.UseCompression = true
|
||||
c.AllowLocalDiscovery = true
|
||||
c.CurveType = "p521"
|
||||
c.WebsocketAddress = "wss://croc3.schollz.com"
|
||||
c.ServerPort = "8130"
|
||||
c.rs.Lock()
|
||||
c.rs.channel = make(map[string]*channelData)
|
||||
c.rs.ips = make(map[string]string)
|
||||
c.cs.channel = new(channelData)
|
||||
c.rs.Unlock()
|
||||
c.localIP = getLocalIP()
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) SetDebug(debug bool) {
|
||||
if debug {
|
||||
SetLogLevel("debug")
|
||||
} else {
|
||||
SetLogLevel("error")
|
||||
}
|
||||
}
|
||||
|
||||
type relayState struct {
|
||||
ips map[string]string
|
||||
channel map[string]*channelData
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type clientState struct {
|
||||
channel *channelData
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type FileMetaData struct {
|
||||
Name string
|
||||
Size int
|
||||
Hash string
|
||||
IsDir bool
|
||||
IsEncrypted bool
|
||||
IsCompressed bool
|
||||
DeleteAfterSending bool
|
||||
}
|
||||
|
||||
type channelData struct {
|
||||
// Relay actions
|
||||
// Open set to true when trying to open
|
||||
Open bool `json:"open"`
|
||||
// Update set to true when updating
|
||||
Update bool `json:"update"`
|
||||
// Close set to true when closing:
|
||||
Close bool `json:"close"`
|
||||
|
||||
// Public
|
||||
// Channel is the name of the channel
|
||||
Channel string `json:"channel,omitempty"`
|
||||
// Pake contains the information for
|
||||
// generating the session key over an insecure channel
|
||||
Pake *pake.Pake `json:"pake"`
|
||||
// TransferReady is set by the relaying when both parties have connected
|
||||
// with their credentials
|
||||
TransferReady bool `json:"transfer_ready"`
|
||||
// Ports returns which TCP ports to connect to
|
||||
Ports []string `json:"ports"`
|
||||
// Curve is the type of elliptic curve to use
|
||||
Curve string `json:"curve"`
|
||||
// FileMetaData is sent after confirmed
|
||||
EncryptedFileMetaData encryption `json:"encrypted_meta_data"`
|
||||
// FileReceived specifies that everything was done right
|
||||
FileReceived bool `json:"file_received"`
|
||||
// ReadyToRead means that the recipient is ready to read
|
||||
ReadyToRead bool `json:"ready_to_read"`
|
||||
// Error is sent if there is an error
|
||||
Error string `json:"error"`
|
||||
// Addresses of the sender and recipient, as determined by the relay
|
||||
Addresses [2]string `json:"addresses"`
|
||||
|
||||
// Sent on initialization, specific to a single user
|
||||
// UUID is sent out only to one person at a time
|
||||
UUID string `json:"uuid"`
|
||||
// Role is the role the person will play
|
||||
Role int `json:"role"`
|
||||
|
||||
// Private
|
||||
// client parameters
|
||||
// codePhrase uses the first 3 characters to establish a channel, and the rest
|
||||
// to form the passphrase
|
||||
codePhrase string
|
||||
// passPhrase is used to generate a session key
|
||||
passPhrase string
|
||||
// sessionKey
|
||||
sessionKey []byte
|
||||
// isReady specifies whether the current client
|
||||
isReady bool
|
||||
fileReady bool
|
||||
fileMetaData FileMetaData
|
||||
notSentMetaData bool
|
||||
finishedHappy bool
|
||||
filesReady bool
|
||||
|
||||
// ws is the connection that the client has to the relay
|
||||
ws *websocket.Conn
|
||||
|
||||
// relay parameters
|
||||
// isopen determine whether or not the channel has been opened
|
||||
isopen bool
|
||||
// store a UUID of the parties to prevent other parties from joining
|
||||
uuids [2]string // 0 is sender, 1 is recipient
|
||||
// connection information is stored when the clients do connect over TCP
|
||||
connection map[string][2]net.Conn
|
||||
// websocket connections
|
||||
websocketConn [2]*websocket.Conn
|
||||
// startTime is the time that the channel was opened
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func (cd channelData) String2() string {
|
||||
cdb, _ := json.Marshal(cd)
|
||||
return string(cdb)
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
func promptCodePhrase() string {
|
||||
return getInput("Enter receive code: ")
|
||||
}
|
||||
|
||||
func promptOkayToRecieve(f FileMetaData) (ok bool) {
|
||||
overwritingOrReceiving := "Receiving"
|
||||
if exists(f.Name) {
|
||||
overwritingOrReceiving = "Overwriting"
|
||||
}
|
||||
fileOrFolder := "file"
|
||||
if f.IsDir {
|
||||
fileOrFolder = "folder"
|
||||
}
|
||||
return "y" == getInput(fmt.Sprintf(
|
||||
`%s %s (%s) into: %s
|
||||
ok? (y/N): `,
|
||||
overwritingOrReceiving,
|
||||
fileOrFolder,
|
||||
humanize.Bytes(uint64(f.Size)),
|
||||
f.Name,
|
||||
))
|
||||
}
|
||||
|
||||
func showIntro(code string, f FileMetaData) {
|
||||
fileOrFolder := "file"
|
||||
if f.IsDir {
|
||||
fileOrFolder = "folder"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr,
|
||||
`Sending %s %s named '%s'
|
||||
Code is: %s
|
||||
On the other computer, please run:
|
||||
|
||||
croc %s
|
||||
`,
|
||||
humanize.Bytes(uint64(f.Size)),
|
||||
fileOrFolder,
|
||||
f.Name,
|
||||
code,
|
||||
code,
|
||||
)
|
||||
}
|
||||
|
||||
func getInput(prompt string) string {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Fprintf(os.Stderr, "%s", prompt)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Croc) startRelay() {
|
||||
ports := c.TcpPorts
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(ports))
|
||||
for _, port := range ports {
|
||||
go func(port string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
log.Debugf("listening on port %s", port)
|
||||
if err := c.listener(port); err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
}(port, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (c *Croc) listener(port string) (err error) {
|
||||
server, err := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error listening on :"+port)
|
||||
}
|
||||
defer server.Close()
|
||||
// spawn a new goroutine whenever a client connects
|
||||
for {
|
||||
connection, err := server.Accept()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "problem accepting connection")
|
||||
}
|
||||
log.Debugf("client %s connected", connection.RemoteAddr().String())
|
||||
go func(port string, connection net.Conn) {
|
||||
errCommunication := c.clientCommuncation(port, connection)
|
||||
if errCommunication != nil {
|
||||
log.Warnf("relay-%s: %s", connection.RemoteAddr().String(), errCommunication.Error())
|
||||
}
|
||||
}(port, connection)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Croc) clientCommuncation(port string, connection net.Conn) (err error) {
|
||||
var con1, con2 net.Conn
|
||||
|
||||
// get the channel and UUID from the client
|
||||
err = sendMessage("channel and uuid?", connection)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
channel, err := receiveMessage(connection)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uuid, err := receiveMessage(connection)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Debugf("%s connected to port %s on channel %s and uuid %s", connection.RemoteAddr().String(), port, channel, uuid)
|
||||
|
||||
// validate channel and UUID
|
||||
c.rs.Lock()
|
||||
if _, ok := c.rs.channel[channel]; !ok {
|
||||
c.rs.Unlock()
|
||||
err = errors.Errorf("channel %s does not exist", channel)
|
||||
return
|
||||
}
|
||||
if uuid != c.rs.channel[channel].uuids[0] &&
|
||||
uuid != c.rs.channel[channel].uuids[1] {
|
||||
c.rs.Unlock()
|
||||
err = errors.Errorf("uuid '%s' is invalid", uuid)
|
||||
return
|
||||
}
|
||||
role := 0
|
||||
if uuid == c.rs.channel[channel].uuids[1] {
|
||||
role = 1
|
||||
}
|
||||
|
||||
if _, ok := c.rs.channel[channel].connection[port]; !ok {
|
||||
c.rs.channel[channel].connection[port] = [2]net.Conn{nil, nil}
|
||||
}
|
||||
con1 = c.rs.channel[channel].connection[port][0]
|
||||
con2 = c.rs.channel[channel].connection[port][1]
|
||||
if role == 0 {
|
||||
con1 = connection
|
||||
} else {
|
||||
con2 = connection
|
||||
}
|
||||
log.Debug(c.rs.channel[channel].connection[port])
|
||||
c.rs.channel[channel].connection[port] = [2]net.Conn{con1, con2}
|
||||
ports := c.rs.channel[channel].Ports
|
||||
c.rs.Unlock()
|
||||
|
||||
if con1 != nil && con2 != nil {
|
||||
log.Debugf("beginning the piping")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
// start piping
|
||||
go func(con1 net.Conn, con2 net.Conn, wg *sync.WaitGroup) {
|
||||
pipe(con1, con2)
|
||||
wg.Done()
|
||||
log.Debug("done piping")
|
||||
}(con1, con2, &wg)
|
||||
|
||||
if port == ports[0] {
|
||||
// then set transfer ready
|
||||
c.rs.Lock()
|
||||
c.rs.channel[channel].TransferReady = true
|
||||
c.rs.channel[channel].websocketConn[0].WriteJSON(c.rs.channel[channel])
|
||||
c.rs.channel[channel].websocketConn[1].WriteJSON(c.rs.channel[channel])
|
||||
c.rs.Unlock()
|
||||
log.Debugf("sent ready signal")
|
||||
}
|
||||
wg.Wait()
|
||||
log.Debugf("finished transfer")
|
||||
}
|
||||
log.Debug("finished client communication")
|
||||
return
|
||||
}
|
||||
|
||||
func sendMessage(message string, connection net.Conn) (err error) {
|
||||
message = fillString(message, bufferSize)
|
||||
_, err = connection.Write([]byte(message))
|
||||
return
|
||||
}
|
||||
|
||||
func receiveMessage(connection net.Conn) (s string, err error) {
|
||||
messageByte := make([]byte, bufferSize)
|
||||
err = connection.SetReadDeadline(time.Now().Add(60 * time.Minute))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = connection.SetDeadline(time.Now().Add(60 * time.Minute))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = connection.SetWriteDeadline(time.Now().Add(60 * time.Minute))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = connection.Read(messageByte)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s = strings.TrimRight(string(messageByte), ":")
|
||||
return
|
||||
}
|
||||
|
||||
func fillString(returnString string, toLength int) string {
|
||||
for {
|
||||
lengthString := len(returnString)
|
||||
if lengthString < toLength {
|
||||
returnString = returnString + ":"
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return returnString
|
||||
}
|
||||
|
||||
// chanFromConn creates a channel from a Conn object, and sends everything it
|
||||
// Read()s from the socket to the channel.
|
||||
func chanFromConn(conn net.Conn) chan []byte {
|
||||
c := make(chan []byte)
|
||||
|
||||
go func() {
|
||||
b := make([]byte, bufferSize)
|
||||
|
||||
for {
|
||||
n, err := conn.Read(b)
|
||||
if n > 0 {
|
||||
res := make([]byte, n)
|
||||
// Copy the buffer so it doesn't get changed while read by the recipient.
|
||||
copy(res, b[:n])
|
||||
c <- res
|
||||
}
|
||||
if err != nil {
|
||||
c <- nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// pipe creates a full-duplex pipe between the two sockets and
|
||||
// transfers data from one to the other.
|
||||
func pipe(conn1 net.Conn, conn2 net.Conn) {
|
||||
chan1 := chanFromConn(conn1)
|
||||
chan2 := chanFromConn(conn2)
|
||||
|
||||
for {
|
||||
select {
|
||||
case b1 := <-chan1:
|
||||
if b1 == nil {
|
||||
return
|
||||
}
|
||||
conn2.Write(b1)
|
||||
|
||||
case b2 := <-chan2:
|
||||
if b2 == nil {
|
||||
return
|
||||
}
|
||||
conn1.Write(b2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
"github.com/frankenbeanies/uuid4"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/schollz/pake"
|
||||
)
|
||||
|
||||
// startServer initiates the server which listens for websocket connections
|
||||
func (c *Croc) startServer() (err error) {
|
||||
// start cleanup on dangling channels
|
||||
go c.channelCleanup()
|
||||
|
||||
var upgrader = websocket.Upgrader{} // use default options
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// incoming websocket request
|
||||
log.Debugf("connecting remote addr: %+v", r.RemoteAddr)
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
address := r.RemoteAddr
|
||||
if _, ok := r.Header["X-Forwarded-For"]; ok {
|
||||
address = r.Header["X-Forwarded-For"][0]
|
||||
}
|
||||
if _, ok := r.Header["X-Real-Ip"]; ok {
|
||||
address = r.Header["X-Real-Ip"][0]
|
||||
}
|
||||
c.rs.Lock()
|
||||
c.rs.ips[ws.RemoteAddr().String()] = address
|
||||
c.rs.Unlock()
|
||||
log.Debugf("connecting remote addr: %s", address)
|
||||
if err != nil {
|
||||
log.Error("upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
var channel string
|
||||
for {
|
||||
log.Debug("waiting for next message")
|
||||
var cd channelData
|
||||
err := ws.ReadJSON(&cd)
|
||||
if err != nil {
|
||||
if _, ok := err.(*websocket.CloseError); ok {
|
||||
// on forced close, delete the channel
|
||||
log.Debug("closed channel")
|
||||
c.closeChannel(channel)
|
||||
} else {
|
||||
log.Debugf("read:", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
channel, err = c.processPayload(ws, cd)
|
||||
if err != nil {
|
||||
// if error, send the error back and then delete the channel
|
||||
log.Warn("problem processing payload %+v: %s", cd, err.Error())
|
||||
ws.WriteJSON(channelData{Error: err.Error()})
|
||||
c.closeChannel(channel)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
log.Debugf("listening on port %s", c.ServerPort)
|
||||
err = http.ListenAndServe(":"+c.ServerPort, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) updateChannel(cd channelData) (err error) {
|
||||
c.rs.Lock()
|
||||
defer c.rs.Unlock()
|
||||
|
||||
// determine if channel is invalid
|
||||
if _, ok := c.rs.channel[cd.Channel]; !ok {
|
||||
err = errors.Errorf("channel '%s' does not exist", cd.Channel)
|
||||
return
|
||||
}
|
||||
|
||||
// determine if UUID is invalid for channel
|
||||
if cd.UUID != c.rs.channel[cd.Channel].uuids[0] &&
|
||||
cd.UUID != c.rs.channel[cd.Channel].uuids[1] {
|
||||
err = errors.Errorf("uuid '%s' is invalid", cd.UUID)
|
||||
return
|
||||
}
|
||||
|
||||
// update each
|
||||
c.rs.channel[cd.Channel].Error = cd.Error
|
||||
c.rs.channel[cd.Channel].FileReceived = cd.FileReceived
|
||||
c.rs.channel[cd.Channel].EncryptedFileMetaData = cd.EncryptedFileMetaData
|
||||
c.rs.channel[cd.Channel].ReadyToRead = cd.ReadyToRead
|
||||
if c.rs.channel[cd.Channel].Pake == nil {
|
||||
c.rs.channel[cd.Channel].Pake = new(pake.Pake)
|
||||
}
|
||||
c.rs.channel[cd.Channel].Pake.HkA = cd.Pake.HkA
|
||||
c.rs.channel[cd.Channel].Pake.HkB = cd.Pake.HkB
|
||||
c.rs.channel[cd.Channel].Pake.Role = cd.Pake.Role
|
||||
c.rs.channel[cd.Channel].Pake.Uᵤ = cd.Pake.Uᵤ
|
||||
c.rs.channel[cd.Channel].Pake.Uᵥ = cd.Pake.Uᵥ
|
||||
c.rs.channel[cd.Channel].Pake.Vᵤ = cd.Pake.Vᵤ
|
||||
c.rs.channel[cd.Channel].Pake.Vᵥ = cd.Pake.Vᵥ
|
||||
c.rs.channel[cd.Channel].Pake.Xᵤ = cd.Pake.Xᵤ
|
||||
c.rs.channel[cd.Channel].Pake.Xᵥ = cd.Pake.Xᵥ
|
||||
c.rs.channel[cd.Channel].Pake.Yᵤ = cd.Pake.Yᵤ
|
||||
c.rs.channel[cd.Channel].Pake.Yᵥ = cd.Pake.Yᵥ
|
||||
if cd.Addresses[0] != "" {
|
||||
c.rs.channel[cd.Channel].Addresses[0] = cd.Addresses[0]
|
||||
}
|
||||
if cd.Addresses[1] != "" {
|
||||
c.rs.channel[cd.Channel].Addresses[1] = cd.Addresses[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) joinChannel(ws *websocket.Conn, cd channelData) (channel string, err error) {
|
||||
log.Debugf("joining channel %s", ws.RemoteAddr().String())
|
||||
c.rs.Lock()
|
||||
defer c.rs.Unlock()
|
||||
|
||||
// determine if sender or recipient
|
||||
if cd.Role != 0 && cd.Role != 1 {
|
||||
err = errors.Errorf("no such role of %d", cd.Role)
|
||||
return
|
||||
}
|
||||
|
||||
// determine channel
|
||||
if cd.Channel == "" {
|
||||
// TODO:
|
||||
// find an empty channel
|
||||
cd.Channel = "chou"
|
||||
}
|
||||
if _, ok := c.rs.channel[cd.Channel]; ok {
|
||||
// channel is not empty
|
||||
if c.rs.channel[cd.Channel].uuids[cd.Role] != "" {
|
||||
err = errors.Errorf("channel '%s' already occupied by role %d", cd.Channel, cd.Role)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Debug("creating new channel")
|
||||
if _, ok := c.rs.channel[cd.Channel]; !ok {
|
||||
c.rs.channel[cd.Channel] = new(channelData)
|
||||
c.rs.channel[cd.Channel].connection = make(map[string][2]net.Conn)
|
||||
}
|
||||
channel = cd.Channel
|
||||
|
||||
// assign UUID for the role in the channel
|
||||
c.rs.channel[cd.Channel].uuids[cd.Role] = uuid4.New().String()
|
||||
log.Debugf("(%s) %s has joined as role %d", cd.Channel, c.rs.channel[cd.Channel].uuids[cd.Role], cd.Role)
|
||||
// send Channel+UUID back to the current person
|
||||
err = ws.WriteJSON(channelData{
|
||||
Channel: cd.Channel,
|
||||
UUID: c.rs.channel[cd.Channel].uuids[cd.Role],
|
||||
Role: cd.Role,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if channel is not open, set initial parameters
|
||||
if !c.rs.channel[cd.Channel].isopen {
|
||||
c.rs.channel[cd.Channel].isopen = true
|
||||
c.rs.channel[cd.Channel].Ports = c.TcpPorts
|
||||
c.rs.channel[cd.Channel].startTime = time.Now()
|
||||
c.rs.channel[cd.Channel].Curve = "p256"
|
||||
}
|
||||
c.rs.channel[cd.Channel].websocketConn[cd.Role] = ws
|
||||
// assign the name
|
||||
c.rs.channel[cd.Channel].Addresses[cd.Role] = c.rs.ips[ws.RemoteAddr().String()]
|
||||
log.Debugf("assigned role %d in channel '%s'", cd.Role, cd.Channel)
|
||||
return
|
||||
}
|
||||
|
||||
// closeChannel will shut down current open websockets and delete the channel information
|
||||
func (c *Croc) closeChannel(channel string) {
|
||||
c.rs.Lock()
|
||||
defer c.rs.Unlock()
|
||||
// check if channel exists
|
||||
if _, ok := c.rs.channel[channel]; !ok {
|
||||
return
|
||||
}
|
||||
// close open connections
|
||||
for _, wsConn := range c.rs.channel[channel].websocketConn {
|
||||
if wsConn != nil {
|
||||
wsConn.Close()
|
||||
delete(c.rs.ips, wsConn.RemoteAddr().String())
|
||||
}
|
||||
}
|
||||
// delete
|
||||
delete(c.rs.channel, channel)
|
||||
}
|
||||
|
||||
func (c *Croc) processPayload(ws *websocket.Conn, cd channelData) (channel string, err error) {
|
||||
log.Debugf("processing payload from %s", ws.RemoteAddr().String())
|
||||
channel = cd.Channel
|
||||
|
||||
// if the request is to close, delete the channel
|
||||
if cd.Close {
|
||||
log.Debugf("closing channel %s", cd.Channel)
|
||||
c.closeChannel(cd.Channel)
|
||||
return
|
||||
}
|
||||
|
||||
// if request is to Open, try to open
|
||||
if cd.Open {
|
||||
channel, err = c.joinChannel(ws, cd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check if open, otherwise return error
|
||||
c.rs.Lock()
|
||||
if _, ok := c.rs.channel[channel]; ok {
|
||||
if !c.rs.channel[channel].isopen {
|
||||
err = errors.Errorf("channel %s is not open, need to open first", channel)
|
||||
c.rs.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.rs.Unlock()
|
||||
|
||||
// if the request is to Update, then update the state
|
||||
if cd.Update {
|
||||
// update
|
||||
err = c.updateChannel(cd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// relay state logic here
|
||||
|
||||
// send out the data to both sender + receiver each time
|
||||
c.rs.Lock()
|
||||
if _, ok := c.rs.channel[channel]; ok {
|
||||
for role, wsConn := range c.rs.channel[channel].websocketConn {
|
||||
if wsConn == nil {
|
||||
continue
|
||||
}
|
||||
log.Debugf("writing latest data %+v to %d", c.rs.channel[channel].String2(), role)
|
||||
err = wsConn.WriteJSON(c.rs.channel[channel])
|
||||
if err != nil {
|
||||
log.Debugf("problem writing to role %d: %s", role, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
c.rs.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Croc) channelCleanup() {
|
||||
maximumWait := 10 * time.Minute
|
||||
for {
|
||||
c.rs.Lock()
|
||||
keys := make([]string, len(c.rs.channel))
|
||||
i := 0
|
||||
for key := range c.rs.channel {
|
||||
keys[i] = key
|
||||
i++
|
||||
}
|
||||
channelsToDelete := []string{}
|
||||
for _, key := range keys {
|
||||
if time.Since(c.rs.channel[key].startTime) > maximumWait {
|
||||
channelsToDelete = append(channelsToDelete, key)
|
||||
}
|
||||
}
|
||||
c.rs.Unlock()
|
||||
|
||||
for _, channel := range channelsToDelete {
|
||||
log.Debugf("channel %s has exceeded time, deleting", channel)
|
||||
c.closeChannel(channel)
|
||||
}
|
||||
time.Sleep(1 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,25 @@
|
|||
package main
|
||||
package croc
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
math_rand "math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/schollz/pake"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/schollz/bytetoword"
|
||||
"github.com/tscholl2/siec"
|
||||
)
|
||||
|
||||
// HashWords returns word after hashing
|
||||
func HashWords(s string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(s))
|
||||
return bytetoword.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
// CatFiles copies data from n files to a single one and removes source files
|
||||
// catFiles copies data from n files to a single one and removes source files
|
||||
// if Debug mode is set to false
|
||||
func CatFiles(files []string, outfile string, remove bool) error {
|
||||
func catFiles(files []string, outfile string, remove bool) error {
|
||||
finished, err := os.Create(outfile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "CatFiles create: ")
|
||||
|
|
@ -47,8 +42,8 @@ func CatFiles(files []string, outfile string, remove bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SplitFile creates a bunch of smaller files with the data from source splited into them
|
||||
func SplitFile(fileName string, numPieces int) (err error) {
|
||||
// splitFile creates a bunch of smaller files with the data from source splited into them
|
||||
func splitFile(fileName string, numPieces int) (err error) {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -79,10 +74,26 @@ func SplitFile(fileName string, numPieces int) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from src to dst. If src and dst files exist, and are
|
||||
func getCurve(s string) (curve pake.EllipticCurve) {
|
||||
switch s {
|
||||
case "p224":
|
||||
curve = elliptic.P224()
|
||||
case "p256":
|
||||
curve = elliptic.P256()
|
||||
case "p384":
|
||||
curve = elliptic.P384()
|
||||
case "p521":
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
curve = siec.SIEC255()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst. If src and dst files exist, and are
|
||||
// the same, then return success. Otherise, attempt to create a hard link
|
||||
// between the two files. If that fail, copy the file contents from src to dst.
|
||||
func CopyFile(src, dst string) (err error) {
|
||||
func copyFile(src, dst string) (err error) {
|
||||
sfi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
@ -139,9 +150,9 @@ func copyFileContents(src, dst string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// HashFile does a md5 hash on the file
|
||||
// hashFile does a md5 hash on the file
|
||||
// from https://golang.org/pkg/crypto/md5/#example_New_file
|
||||
func HashFile(filename string) (hash string, err error) {
|
||||
func hashFile(filename string) (hash string, err error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
@ -156,8 +167,8 @@ func HashFile(filename string) (hash string, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// FileSize returns the size of a file
|
||||
func FileSize(filename string) (int, error) {
|
||||
// fileSize returns the size of a file
|
||||
func fileSize(filename string) (int, error) {
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
|
|
@ -166,8 +177,8 @@ func FileSize(filename string) (int, error) {
|
|||
return size, nil
|
||||
}
|
||||
|
||||
// GetLocalIP returns the local ip address
|
||||
func GetLocalIP() string {
|
||||
// getLocalIP returns the local ip address
|
||||
func getLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
|
|
@ -184,31 +195,21 @@ func GetLocalIP() string {
|
|||
return bestIP
|
||||
}
|
||||
|
||||
// src is seeds the random generator for generating random strings
|
||||
var src = math_rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
// RandStringBytesMaskImprSrc prints a random string
|
||||
func RandStringBytesMaskImprSrc(n int) string {
|
||||
b := make([]byte, n)
|
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = src.Int63(), letterIdxMax
|
||||
// exists reports whether the named file or directory exists.
|
||||
func exists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
b[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
|
||||
return string(b)
|
||||
return true
|
||||
}
|
||||
|
||||
func tempFileName(prefix string) string {
|
||||
var f *os.File
|
||||
f, _ = ioutil.TempFile(".", prefix)
|
||||
name := f.Name()
|
||||
f.Close()
|
||||
os.Remove(name)
|
||||
return name
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package croc
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSplitFile(t *testing.T) {
|
||||
err := SplitFile("testing_data/README.md", 3)
|
||||
err := splitFile("testing_data/README.md", 3)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -16,13 +16,13 @@ func TestSplitFile(t *testing.T) {
|
|||
|
||||
func TestFileSize(t *testing.T) {
|
||||
t.Run("File is ok ", func(t *testing.T) {
|
||||
_, err := FileSize("testing_data/README.md")
|
||||
_, err := fileSize("testing_data/README.md")
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("File does not exist", func(t *testing.T) {
|
||||
s, err := FileSize("testing_data/someStrangeFile")
|
||||
s, err := fileSize("testing_data/someStrangeFile")
|
||||
if err == nil {
|
||||
t.Error("should return an error")
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ func TestFileSize(t *testing.T) {
|
|||
|
||||
func TestHashFile(t *testing.T) {
|
||||
t.Run("Hash created successfully", func(t *testing.T) {
|
||||
h, err := HashFile("testing_data/README.md")
|
||||
h, err := hashFile("testing_data/README.md")
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ func TestHashFile(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("File does not exist", func(t *testing.T) {
|
||||
h, err := HashFile("testing_data/someStrangeFile")
|
||||
h, err := hashFile("testing_data/someStrangeFile")
|
||||
if err == nil {
|
||||
t.Error("should return an error")
|
||||
}
|
||||
|
|
@ -64,11 +64,11 @@ func TestCopyFileContents(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
f1Length, err := FileSize(f1)
|
||||
f1Length, err := fileSize(f1)
|
||||
if err != nil {
|
||||
t.Errorf("can't get file nr1 size: %v", err)
|
||||
}
|
||||
f2Length, err := FileSize(f2)
|
||||
f2Length, err := fileSize(f2)
|
||||
if err != nil {
|
||||
t.Errorf("can't get file nr2 size: %v", err)
|
||||
}
|
||||
|
|
@ -84,15 +84,15 @@ func TestCopyFile(t *testing.T) {
|
|||
t.Run("Files copied successfully", func(t *testing.T) {
|
||||
f1 := "testing_data/README.md"
|
||||
f2 := "testing_data/CopyOfREADME.md"
|
||||
err := CopyFile(f1, f2)
|
||||
err := copyFile(f1, f2)
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
f1Length, err := FileSize(f1)
|
||||
f1Length, err := fileSize(f1)
|
||||
if err != nil {
|
||||
t.Errorf("can't get file nr1 size: %v", err)
|
||||
}
|
||||
f2Length, err := FileSize(f2)
|
||||
f2Length, err := fileSize(f2)
|
||||
if err != nil {
|
||||
t.Errorf("can't get file nr2 size: %v", err)
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ func TestCopyFile(t *testing.T) {
|
|||
func TestCatFiles(t *testing.T) {
|
||||
t.Run("CatFiles passing", func(t *testing.T) {
|
||||
files := []string{"testing_data/catFile1.txt", "testing_data/catFile2.txt"}
|
||||
err := CatFiles(files, "testing_data/CatFile.txt", false)
|
||||
err := catFiles(files, "testing_data/CatFile.txt", false)
|
||||
if err != nil {
|
||||
t.Errorf("should pass with no error, got: %v", err)
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"compress/flate"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
)
|
||||
|
||||
func unzipFile(src, dest string) (err error) {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
var rc io.ReadCloser
|
||||
rc, err = f.Open()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Store filename/path for returning and using later on
|
||||
fpath := filepath.Join(dest, f.Name)
|
||||
log.Debugf("unzipping %s", fpath)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
|
||||
// Make Folder
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
|
||||
} else {
|
||||
|
||||
// Make File
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var outFile *os.File
|
||||
outFile, err = os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
|
||||
// Close the file without defer to close before next iteration of loop
|
||||
outFile.Close()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
log.Debugf("unzipped %s to %s", src, dest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func zipFile(fname string, compress bool) (writtenFilename string, err error) {
|
||||
log.Debugf("zipping %s with compression? %v", fname, compress)
|
||||
pathtofile, filename := filepath.Split(fname)
|
||||
curdir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
curdir, err = filepath.Abs(curdir)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
log.Debugf("current directory: %s", curdir)
|
||||
newfile, err := ioutil.TempFile(curdir, "croc-zipped")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
writtenFilename = newfile.Name()
|
||||
defer newfile.Close()
|
||||
|
||||
defer os.Chdir(curdir)
|
||||
log.Debugf("changing dir to %s", pathtofile)
|
||||
os.Chdir(pathtofile)
|
||||
|
||||
zipWriter := zip.NewWriter(newfile)
|
||||
zipWriter.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
|
||||
if compress {
|
||||
return flate.NewWriter(out, flate.BestCompression)
|
||||
} else {
|
||||
return flate.NewWriter(out, flate.NoCompression)
|
||||
}
|
||||
})
|
||||
defer zipWriter.Close()
|
||||
|
||||
zipfile, err := os.Open(filename)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
defer zipfile.Close()
|
||||
// Get the file information
|
||||
info, err := zipfile.Stat()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// write header informaiton
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
if info.IsDir() {
|
||||
baseDir := filename
|
||||
filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if baseDir != "" {
|
||||
header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, baseDir))
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
header.Name += "/"
|
||||
} else {
|
||||
header.Method = zip.Deflate
|
||||
}
|
||||
|
||||
writer, err = zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(writer, file)
|
||||
return err
|
||||
})
|
||||
} else {
|
||||
writer, err = zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(writer, zipfile)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("wrote zip file to %s", writtenFilename)
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package croc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
log "github.com/cihub/seelog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestZip(t *testing.T) {
|
||||
defer log.Flush()
|
||||
writtenFilename, err := zipFile("../README.md", false)
|
||||
assert.Nil(t, err)
|
||||
defer os.Remove(writtenFilename)
|
||||
|
||||
err = unzipFile(writtenFilename, ".")
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, exists("README.md"))
|
||||
os.RemoveAll("README.md")
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# Created by .gitignore support plugin (hsz.mobi)
|
||||
### Go template
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
.idea
|
||||
*.iml
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.6.3
|
||||
- 1.7.3
|
||||
env:
|
||||
- GOARCH: amd64
|
||||
- GOARCH: 386
|
||||
script:
|
||||
- go test -v
|
||||
notifications:
|
||||
email:
|
||||
recipients:
|
||||
- brian.downs@gmail.com
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
# Spinner
|
||||
|
||||
[](https://godoc.org/github.com/briandowns/spinner) [](https://travis-ci.org/briandowns/spinner)
|
||||
|
||||
spinner is a simple package to add a spinner / progress indicator to any terminal application. Examples can be found below as well as full examples in the examples directory.
|
||||
|
||||
For more detail about the library and its features, reference your local godoc once installed.
|
||||
|
||||
Contributions welcome!
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/briandowns/spinner
|
||||
```
|
||||
|
||||
## Available Character Sets
|
||||
(Numbered by their slice index)
|
||||
|
||||
index | character set
|
||||
------|---------------
|
||||
0 | ```←↖↑↗→↘↓↙```
|
||||
1 | ```▁▃▄▅▆▇█▇▆▅▄▃▁```
|
||||
2 | ```▖▘▝▗```
|
||||
3 | ```┤┘┴└├┌┬┐```
|
||||
4 | ```◢◣◤◥```
|
||||
5 | ```◰◳◲◱```
|
||||
6 | ```◴◷◶◵```
|
||||
7 | ```◐◓◑◒```
|
||||
8 | ```.oO@*```
|
||||
9 | ```|/-\```
|
||||
10 | ```◡◡⊙⊙◠◠```
|
||||
11 | ```⣾⣽⣻⢿⡿⣟⣯⣷```
|
||||
12 | ```>))'> >))'> >))'> >))'> >))'> <'((< <'((< <'((<```
|
||||
13 | ```⠁⠂⠄⡀⢀⠠⠐⠈```
|
||||
14 | ```⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏```
|
||||
15 | ```abcdefghijklmnopqrstuvwxyz```
|
||||
16 | ```▉▊▋▌▍▎▏▎▍▌▋▊▉```
|
||||
17 | ```■□▪▫```
|
||||
18 | ```←↑→↓```
|
||||
19 | ```╫╪```
|
||||
20 | ```⇐⇖⇑⇗⇒⇘⇓⇙```
|
||||
21 | ```⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈```
|
||||
22 | ```⠈⠉⠋⠓⠒⠐⠐⠒⠖⠦⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈```
|
||||
23 | ```⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠴⠲⠒⠂⠂⠒⠚⠙⠉⠁```
|
||||
24 | ```⠋⠙⠚⠒⠂⠂⠒⠲⠴⠦⠖⠒⠐⠐⠒⠓⠋```
|
||||
25 | ```ヲァィゥェォャュョッアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン```
|
||||
26 | ```. .. ...```
|
||||
27 | ```▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▏▎▍▌▋▊▉█▇▆▅▄▃▂▁```
|
||||
28 | ```.oO°Oo.```
|
||||
29 | ```+x```
|
||||
30 | ```v<^>```
|
||||
31 | ```>>---> >>---> >>---> >>---> >>---> <---<< <---<< <---<< <---<< <---<<```
|
||||
32 | ```| || ||| |||| ||||| |||||| ||||| |||| ||| || |```
|
||||
33 | ```[] [=] [==] [===] [====] [=====] [======] [=======] [========] [=========] [==========]```
|
||||
34 | ```(*---------) (-*--------) (--*-------) (---*------) (----*-----) (-----*----) (------*---) (-------*--) (--------*-) (---------*)```
|
||||
35 | ```█▒▒▒▒▒▒▒▒▒ ███▒▒▒▒▒▒▒ █████▒▒▒▒▒ ███████▒▒▒ ██████████```
|
||||
36 | ```[ ] [=> ] [===> ] [=====> ] [======> ] [========> ] [==========> ] [============> ] [==============> ] [================> ] [==================> ] [===================>]```
|
||||
37 | ```🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛```
|
||||
38 | ```🕐 🕜 🕑 🕝 🕒 🕞 🕓 🕟 🕔 🕠 🕕 🕡 🕖 🕢 🕗 🕣 🕘 🕤 🕙 🕥 🕚 🕦 🕛 🕧```
|
||||
39 | ```🌍 🌎 🌏```
|
||||
40 | ```◜ ◝ ◞ ◟```
|
||||
41 | ```⬒ ⬔ ⬓ ⬕```
|
||||
42 | ```⬖ ⬘ ⬗ ⬙```
|
||||
43 | ```[>>> >] []>>>> [] [] >>>> [] [] >>>> [] [] >>>> [] [] >>>>[] [>> >>]```
|
||||
|
||||
## Features
|
||||
|
||||
* Start
|
||||
* Stop
|
||||
* Restart
|
||||
* Reverse direction
|
||||
* Update the spinner character set
|
||||
* Update the spinner speed
|
||||
* Prefix or append text
|
||||
* Change spinner color, background, and text attributes such as bold / italics
|
||||
* Get spinner status
|
||||
* Chain, pipe, redirect output
|
||||
* Output final string on spinner/indicator completion
|
||||
|
||||
## Examples
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/briandowns/spinner"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) // Build our new spinner
|
||||
s.Start() // Start the spinner
|
||||
time.Sleep(4 * time.Second) // Run for some time to simulate work
|
||||
s.Stop()
|
||||
}
|
||||
```
|
||||
|
||||
## Update the character set and restart the spinner
|
||||
|
||||
```Go
|
||||
s.UpdateCharSet(spinner.CharSets[1]) // Update spinner to use a different character set
|
||||
s.Restart() // Restart the spinner
|
||||
time.Sleep(4 * time.Second)
|
||||
s.Stop()
|
||||
```
|
||||
|
||||
## Update spin speed and restart the spinner
|
||||
|
||||
```Go
|
||||
s.UpdateSpeed(200 * time.Millisecond) // Update the speed the spinner spins at
|
||||
s.Restart()
|
||||
time.Sleep(4 * time.Second)
|
||||
s.Stop()
|
||||
```
|
||||
|
||||
## Reverse the direction of the spinner
|
||||
|
||||
```Go
|
||||
s.Reverse() // Reverse the direction the spinner is spinning
|
||||
s.Restart()
|
||||
time.Sleep(4 * time.Second)
|
||||
s.Stop()
|
||||
```
|
||||
|
||||
## Provide your own spinner
|
||||
|
||||
(or send me an issue or pull request to add to the project)
|
||||
|
||||
```Go
|
||||
someSet := []string{"+", "-"}
|
||||
s := spinner.New(someSet, 100*time.Millisecond)
|
||||
```
|
||||
|
||||
## Prefix or append text to the spinner
|
||||
|
||||
```Go
|
||||
s.Prefix = "prefixed text: " // Prefix text before the spinner
|
||||
s.Suffix = " :appended text" // Append text after the spinner
|
||||
```
|
||||
|
||||
## Set or change the color of the spinner. Default color is white. This will restart the spinner with the new color.
|
||||
|
||||
```Go
|
||||
s.Color("red") // Set the spinner color to red
|
||||
```
|
||||
|
||||
You can specify both the background and foreground color, as well as additional attributes such as `bold` or `underline`.
|
||||
|
||||
```Go
|
||||
s.Color("red", "bold") // Set the spinner color to a bold red
|
||||
```
|
||||
|
||||
Or to set the background to black, the foreground to a bold red:
|
||||
|
||||
```Go
|
||||
s.Color("bgBlack", "bold", "fgRed")
|
||||
```
|
||||
|
||||
Below is the full color and attribute list:
|
||||
|
||||
```
|
||||
// default colors
|
||||
red
|
||||
black
|
||||
green
|
||||
yellow
|
||||
blue
|
||||
magenta
|
||||
cyan
|
||||
white
|
||||
|
||||
// attributes
|
||||
reset
|
||||
bold
|
||||
faint
|
||||
italic
|
||||
underline
|
||||
blinkslow
|
||||
blinkrapid
|
||||
reversevideo
|
||||
concealed
|
||||
crossedout
|
||||
|
||||
// foreground text
|
||||
fgBlack
|
||||
fgRed
|
||||
fgGreen
|
||||
fgYellow
|
||||
fgBlue
|
||||
fgMagenta
|
||||
fgCyan
|
||||
fgWhite
|
||||
|
||||
// foreground Hi-Intensity text
|
||||
fgHiBlack
|
||||
fgHiRed
|
||||
fgHiGreen
|
||||
fgHiYellow
|
||||
fgHiBlue
|
||||
fgHiMagenta
|
||||
fgHiCyan
|
||||
fgHiWhite
|
||||
|
||||
// background text
|
||||
bgBlack
|
||||
bgRed
|
||||
bgGreen
|
||||
bgYellow
|
||||
bgBlue
|
||||
bgMagenta
|
||||
bgCyan
|
||||
bgWhite
|
||||
|
||||
// background Hi-Intensity text
|
||||
bgHiBlack
|
||||
bgHiRed
|
||||
bgHiGreen
|
||||
bgHiYellow
|
||||
bgHiBlue
|
||||
bgHiMagenta
|
||||
bgHiCyan
|
||||
bgHiWhite
|
||||
```
|
||||
|
||||
## Generate a sequence of numbers
|
||||
|
||||
```Go
|
||||
setOfDigits := spinner.GenerateNumberSequence(25) // Generate a 25 digit string of numbers
|
||||
s := spinner.New(setOfDigits, 100*time.Millisecond)
|
||||
```
|
||||
|
||||
## Get spinner status
|
||||
|
||||
```Go
|
||||
fmt.Println(s.Active())
|
||||
```
|
||||
|
||||
## Unix pipe and redirect
|
||||
|
||||
Feature suggested and write up by [dekz](https://github.com/dekz)
|
||||
|
||||
Setting the Spinner Writer to Stderr helps show progress to the user, with the enhancement to chain, pipe or redirect the output.
|
||||
|
||||
```go
|
||||
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
|
||||
s.Suffix = " Encrypting data..."
|
||||
s.Writer = os.Stderr
|
||||
s.Start()
|
||||
// Encrypt the data into ciphertext
|
||||
fmt.Println(os.Stdout, ciphertext)
|
||||
```
|
||||
|
||||
```sh
|
||||
> myprog encrypt "Secret text" > encrypted.txt
|
||||
⣯ Encrypting data...
|
||||
```
|
||||
|
||||
```sh
|
||||
> cat encrypted.txt
|
||||
1243hjkbas23i9ah27sj39jghv237n2oa93hg83
|
||||
```
|
||||
|
||||
## Final String Output
|
||||
|
||||
Add additional output when the spinner/indicator has completed. The "final" output string can be multi-lined and will be written to wherever the `io.Writer` has been configured for.
|
||||
|
||||
```Go
|
||||
s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
|
||||
s.FinalMSG = "Complete!\nNew line!\nAnother one!\n"
|
||||
s.Start()
|
||||
time.Sleep(4 * time.Second)
|
||||
s.Stop()
|
||||
```
|
||||
|
||||
Output
|
||||
```sh
|
||||
Complete!
|
||||
New line!
|
||||
Another one!
|
||||
```
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
package spinner
|
||||
|
||||
const (
|
||||
clockOneOClock = '\U0001F550'
|
||||
clockOneThirty = '\U0001F55C'
|
||||
)
|
||||
|
||||
// CharSets contains the available character sets
|
||||
var CharSets = map[int][]string{
|
||||
0: {"←", "↖", "↑", "↗", "→", "↘", "↓", "↙"},
|
||||
1: {"▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▁"},
|
||||
2: {"▖", "▘", "▝", "▗"},
|
||||
3: {"┤", "┘", "┴", "└", "├", "┌", "┬", "┐"},
|
||||
4: {"◢", "◣", "◤", "◥"},
|
||||
5: {"◰", "◳", "◲", "◱"},
|
||||
6: {"◴", "◷", "◶", "◵"},
|
||||
7: {"◐", "◓", "◑", "◒"},
|
||||
8: {".", "o", "O", "@", "*"},
|
||||
9: {"|", "/", "-", "\\"},
|
||||
10: {"◡◡", "⊙⊙", "◠◠"},
|
||||
11: {"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"},
|
||||
12: {">))'>", " >))'>", " >))'>", " >))'>", " >))'>", " <'((<", " <'((<", " <'((<"},
|
||||
13: {"⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"},
|
||||
14: {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
|
||||
15: {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"},
|
||||
16: {"▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉"},
|
||||
17: {"■", "□", "▪", "▫"},
|
||||
18: {"←", "↑", "→", "↓"},
|
||||
19: {"╫", "╪"},
|
||||
20: {"⇐", "⇖", "⇑", "⇗", "⇒", "⇘", "⇓", "⇙"},
|
||||
21: {"⠁", "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈", "⠈"},
|
||||
22: {"⠈", "⠉", "⠋", "⠓", "⠒", "⠐", "⠐", "⠒", "⠖", "⠦", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈"},
|
||||
23: {"⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠴", "⠲", "⠒", "⠂", "⠂", "⠒", "⠚", "⠙", "⠉", "⠁"},
|
||||
24: {"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋"},
|
||||
25: {"ヲ", "ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ッ", "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ン"},
|
||||
26: {".", "..", "..."},
|
||||
27: {"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"},
|
||||
28: {".", "o", "O", "°", "O", "o", "."},
|
||||
29: {"+", "x"},
|
||||
30: {"v", "<", "^", ">"},
|
||||
31: {">>--->", " >>--->", " >>--->", " >>--->", " >>--->", " <---<<", " <---<<", " <---<<", " <---<<", "<---<<"},
|
||||
32: {"|", "||", "|||", "||||", "|||||", "|||||||", "||||||||", "|||||||", "||||||", "|||||", "||||", "|||", "||", "|"},
|
||||
33: {"[ ]", "[= ]", "[== ]", "[=== ]", "[==== ]", "[===== ]", "[====== ]", "[======= ]", "[======== ]", "[========= ]", "[==========]"},
|
||||
34: {"(*---------)", "(-*--------)", "(--*-------)", "(---*------)", "(----*-----)", "(-----*----)", "(------*---)", "(-------*--)", "(--------*-)", "(---------*)"},
|
||||
35: {"█▒▒▒▒▒▒▒▒▒", "███▒▒▒▒▒▒▒", "█████▒▒▒▒▒", "███████▒▒▒", "██████████"},
|
||||
36: {"[ ]", "[=> ]", "[===> ]", "[=====> ]", "[======> ]", "[========> ]", "[==========> ]", "[============> ]", "[==============> ]", "[================> ]", "[==================> ]", "[===================>]"},
|
||||
39: {"🌍", "🌎", "🌏"},
|
||||
40: {"◜", "◝", "◞", "◟"},
|
||||
41: {"⬒", "⬔", "⬓", "⬕"},
|
||||
42: {"⬖", "⬘", "⬗", "⬙"},
|
||||
43: {"[>>> >]", "[]>>>> []", "[] >>>> []", "[] >>>> []", "[] >>>> []", "[] >>>>[]", "[>> >>]"},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for i := rune(0); i < 12; i++ {
|
||||
CharSets[37] = append(CharSets[37], string([]rune{clockOneOClock + i}))
|
||||
CharSets[38] = append(CharSets[38], string([]rune{clockOneOClock + i}), string([]rune{clockOneThirty + i}))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package spinner is a simple package to add a spinner / progress indicator to any terminal application.
|
||||
package spinner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// errInvalidColor is returned when attempting to set an invalid color
|
||||
var errInvalidColor = errors.New("invalid color")
|
||||
|
||||
// validColors holds an array of the only colors allowed
|
||||
var validColors = map[string]bool{
|
||||
// default colors for backwards compatibility
|
||||
"black": true,
|
||||
"red": true,
|
||||
"green": true,
|
||||
"yellow": true,
|
||||
"blue": true,
|
||||
"magenta": true,
|
||||
"cyan": true,
|
||||
"white": true,
|
||||
|
||||
// attributes
|
||||
"reset": true,
|
||||
"bold": true,
|
||||
"faint": true,
|
||||
"italic": true,
|
||||
"underline": true,
|
||||
"blinkslow": true,
|
||||
"blinkrapid": true,
|
||||
"reversevideo": true,
|
||||
"concealed": true,
|
||||
"crossedout": true,
|
||||
|
||||
// foreground text
|
||||
"fgBlack": true,
|
||||
"fgRed": true,
|
||||
"fgGreen": true,
|
||||
"fgYellow": true,
|
||||
"fgBlue": true,
|
||||
"fgMagenta": true,
|
||||
"fgCyan": true,
|
||||
"fgWhite": true,
|
||||
|
||||
// foreground Hi-Intensity text
|
||||
"fgHiBlack": true,
|
||||
"fgHiRed": true,
|
||||
"fgHiGreen": true,
|
||||
"fgHiYellow": true,
|
||||
"fgHiBlue": true,
|
||||
"fgHiMagenta": true,
|
||||
"fgHiCyan": true,
|
||||
"fgHiWhite": true,
|
||||
|
||||
// background text
|
||||
"bgBlack": true,
|
||||
"bgRed": true,
|
||||
"bgGreen": true,
|
||||
"bgYellow": true,
|
||||
"bgBlue": true,
|
||||
"bgMagenta": true,
|
||||
"bgCyan": true,
|
||||
"bgWhite": true,
|
||||
|
||||
// background Hi-Intensity text
|
||||
"bgHiBlack": true,
|
||||
"bgHiRed": true,
|
||||
"bgHiGreen": true,
|
||||
"bgHiYellow": true,
|
||||
"bgHiBlue": true,
|
||||
"bgHiMagenta": true,
|
||||
"bgHiCyan": true,
|
||||
"bgHiWhite": true,
|
||||
}
|
||||
|
||||
// returns a valid color's foreground text color attribute
|
||||
var colorAttributeMap = map[string]color.Attribute{
|
||||
// default colors for backwards compatibility
|
||||
"black": color.FgBlack,
|
||||
"red": color.FgRed,
|
||||
"green": color.FgGreen,
|
||||
"yellow": color.FgYellow,
|
||||
"blue": color.FgBlue,
|
||||
"magenta": color.FgMagenta,
|
||||
"cyan": color.FgCyan,
|
||||
"white": color.FgWhite,
|
||||
|
||||
// attributes
|
||||
"reset": color.Reset,
|
||||
"bold": color.Bold,
|
||||
"faint": color.Faint,
|
||||
"italic": color.Italic,
|
||||
"underline": color.Underline,
|
||||
"blinkslow": color.BlinkSlow,
|
||||
"blinkrapid": color.BlinkRapid,
|
||||
"reversevideo": color.ReverseVideo,
|
||||
"concealed": color.Concealed,
|
||||
"crossedout": color.CrossedOut,
|
||||
|
||||
// foreground text colors
|
||||
"fgBlack": color.FgBlack,
|
||||
"fgRed": color.FgRed,
|
||||
"fgGreen": color.FgGreen,
|
||||
"fgYellow": color.FgYellow,
|
||||
"fgBlue": color.FgBlue,
|
||||
"fgMagenta": color.FgMagenta,
|
||||
"fgCyan": color.FgCyan,
|
||||
"fgWhite": color.FgWhite,
|
||||
|
||||
// foreground Hi-Intensity text colors
|
||||
"fgHiBlack": color.FgHiBlack,
|
||||
"fgHiRed": color.FgHiRed,
|
||||
"fgHiGreen": color.FgHiGreen,
|
||||
"fgHiYellow": color.FgHiYellow,
|
||||
"fgHiBlue": color.FgHiBlue,
|
||||
"fgHiMagenta": color.FgHiMagenta,
|
||||
"fgHiCyan": color.FgHiCyan,
|
||||
"fgHiWhite": color.FgHiWhite,
|
||||
|
||||
// background text colors
|
||||
"bgBlack": color.BgBlack,
|
||||
"bgRed": color.BgRed,
|
||||
"bgGreen": color.BgGreen,
|
||||
"bgYellow": color.BgYellow,
|
||||
"bgBlue": color.BgBlue,
|
||||
"bgMagenta": color.BgMagenta,
|
||||
"bgCyan": color.BgCyan,
|
||||
"bgWhite": color.BgWhite,
|
||||
|
||||
// background Hi-Intensity text colors
|
||||
"bgHiBlack": color.BgHiBlack,
|
||||
"bgHiRed": color.BgHiRed,
|
||||
"bgHiGreen": color.BgHiGreen,
|
||||
"bgHiYellow": color.BgHiYellow,
|
||||
"bgHiBlue": color.BgHiBlue,
|
||||
"bgHiMagenta": color.BgHiMagenta,
|
||||
"bgHiCyan": color.BgHiCyan,
|
||||
"bgHiWhite": color.BgHiWhite,
|
||||
}
|
||||
|
||||
// validColor will make sure the given color is actually allowed
|
||||
func validColor(c string) bool {
|
||||
valid := false
|
||||
if validColors[c] {
|
||||
valid = true
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
// Spinner struct to hold the provided options
|
||||
type Spinner struct {
|
||||
Delay time.Duration // Delay is the speed of the indicator
|
||||
chars []string // chars holds the chosen character set
|
||||
Prefix string // Prefix is the text preppended to the indicator
|
||||
Suffix string // Suffix is the text appended to the indicator
|
||||
FinalMSG string // string displayed after Stop() is called
|
||||
lastOutput string // last character(set) written
|
||||
color func(a ...interface{}) string // default color is white
|
||||
lock *sync.RWMutex //
|
||||
Writer io.Writer // to make testing better, exported so users have access
|
||||
active bool // active holds the state of the spinner
|
||||
stopChan chan struct{} // stopChan is a channel used to stop the indicator
|
||||
}
|
||||
|
||||
// New provides a pointer to an instance of Spinner with the supplied options
|
||||
func New(cs []string, d time.Duration) *Spinner {
|
||||
return &Spinner{
|
||||
Delay: d,
|
||||
chars: cs,
|
||||
color: color.New(color.FgWhite).SprintFunc(),
|
||||
lock: &sync.RWMutex{},
|
||||
Writer: color.Output,
|
||||
active: false,
|
||||
stopChan: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Active will return whether or not the spinner is currently active
|
||||
func (s *Spinner) Active() bool {
|
||||
return s.active
|
||||
}
|
||||
|
||||
// Start will start the indicator
|
||||
func (s *Spinner) Start() {
|
||||
if s.active {
|
||||
return
|
||||
}
|
||||
s.active = true
|
||||
|
||||
go func() {
|
||||
for {
|
||||
for i := 0; i < len(s.chars); i++ {
|
||||
select {
|
||||
case <-s.stopChan:
|
||||
return
|
||||
default:
|
||||
s.lock.Lock()
|
||||
s.erase()
|
||||
outColor := fmt.Sprintf("%s%s%s ", s.Prefix, s.color(s.chars[i]), s.Suffix)
|
||||
outPlain := fmt.Sprintf("%s%s%s ", s.Prefix, s.chars[i], s.Suffix)
|
||||
fmt.Fprint(s.Writer, outColor)
|
||||
s.lastOutput = outPlain
|
||||
delay := s.Delay
|
||||
s.lock.Unlock()
|
||||
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop stops the indicator
|
||||
func (s *Spinner) Stop() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if s.active {
|
||||
s.active = false
|
||||
s.erase()
|
||||
if s.FinalMSG != "" {
|
||||
fmt.Fprintf(s.Writer, s.FinalMSG)
|
||||
}
|
||||
s.stopChan <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Restart will stop and start the indicator
|
||||
func (s *Spinner) Restart() {
|
||||
s.Stop()
|
||||
s.Start()
|
||||
}
|
||||
|
||||
// Reverse will reverse the order of the slice assigned to the indicator
|
||||
func (s *Spinner) Reverse() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
for i, j := 0, len(s.chars)-1; i < j; i, j = i+1, j-1 {
|
||||
s.chars[i], s.chars[j] = s.chars[j], s.chars[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Color will set the struct field for the given color to be used
|
||||
func (s *Spinner) Color(colors ...string) error {
|
||||
|
||||
colorAttributes := make([]color.Attribute, len(colors))
|
||||
|
||||
// Verify colours are valid and place the appropriate attribute in the array
|
||||
for index, c := range colors {
|
||||
if !validColor(c) {
|
||||
return errInvalidColor
|
||||
}
|
||||
|
||||
colorAttributes[index] = colorAttributeMap[c]
|
||||
}
|
||||
|
||||
s.color = color.New(colorAttributes...).SprintFunc()
|
||||
s.Restart()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSpeed will set the indicator delay to the given value
|
||||
func (s *Spinner) UpdateSpeed(d time.Duration) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.Delay = d
|
||||
}
|
||||
|
||||
// UpdateCharSet will change the current character set to the given one
|
||||
func (s *Spinner) UpdateCharSet(cs []string) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.chars = cs
|
||||
}
|
||||
|
||||
// erase deletes written characters
|
||||
//
|
||||
// Caller must already hold s.lock.
|
||||
func (s *Spinner) erase() {
|
||||
n := utf8.RuneCountInString(s.lastOutput)
|
||||
del, _ := hex.DecodeString("7f")
|
||||
for _, c := range []string{"\b", string(del), "\b"} {
|
||||
for i := 0; i < n; i++ {
|
||||
fmt.Fprintf(s.Writer, c)
|
||||
}
|
||||
}
|
||||
s.lastOutput = ""
|
||||
}
|
||||
|
||||
// GenerateNumberSequence will generate a slice of integers at the
|
||||
// provided length and convert them each to a string
|
||||
func GenerateNumberSequence(length int) []string {
|
||||
numSeq := make([]string, length)
|
||||
for i := 0; i < length; i++ {
|
||||
numSeq[i] = strconv.Itoa(i)
|
||||
}
|
||||
return numSeq
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
Copyright (c) 2012, Cloud Instruments Co., Ltd. <info@cin.io>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the Cloud Instruments Co., Ltd. nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
Seelog
|
||||
=======
|
||||
|
||||
Seelog is a powerful and easy-to-learn logging framework that provides functionality for flexible dispatching, filtering, and formatting log messages.
|
||||
It is natively written in the [Go](http://golang.org/) programming language.
|
||||
|
||||
[](https://drone.io/github.com/cihub/seelog/latest)
|
||||
|
||||
Features
|
||||
------------------
|
||||
|
||||
* Xml configuring to be able to change logger parameters without recompilation
|
||||
* Changing configurations on the fly without app restart
|
||||
* Possibility to set different log configurations for different project files and functions
|
||||
* Adjustable message formatting
|
||||
* Simultaneous log output to multiple streams
|
||||
* Choosing logger priority strategy to minimize performance hit
|
||||
* Different output writers
|
||||
* Console writer
|
||||
* File writer
|
||||
* Buffered writer (Chunk writer)
|
||||
* Rolling log writer (Logging with rotation)
|
||||
* SMTP writer
|
||||
* Others... (See [Wiki](https://github.com/cihub/seelog/wiki))
|
||||
* Log message wrappers (JSON, XML, etc.)
|
||||
* Global variables and functions for easy usage in standalone apps
|
||||
* Functions for flexible usage in libraries
|
||||
|
||||
Quick-start
|
||||
-----------
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import log "github.com/cihub/seelog"
|
||||
|
||||
func main() {
|
||||
defer log.Flush()
|
||||
log.Info("Hello from Seelog!")
|
||||
}
|
||||
```
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
If you don't have the Go development environment installed, visit the
|
||||
[Getting Started](http://golang.org/doc/install.html) document and follow the instructions. Once you're ready, execute the following command:
|
||||
|
||||
```
|
||||
go get -u github.com/cihub/seelog
|
||||
```
|
||||
|
||||
*IMPORTANT*: If you are not using the latest release version of Go, check out this [wiki page](https://github.com/cihub/seelog/wiki/Notes-on-'go-get')
|
||||
|
||||
Documentation
|
||||
---------------
|
||||
|
||||
Seelog has github wiki pages, which contain detailed how-tos references: https://github.com/cihub/seelog/wiki
|
||||
|
||||
Examples
|
||||
---------------
|
||||
|
||||
Seelog examples can be found here: [seelog-examples](https://github.com/cihub/seelog-examples)
|
||||
|
||||
Issues
|
||||
---------------
|
||||
|
||||
Feel free to push issues that could make Seelog better: https://github.com/cihub/seelog/issues
|
||||
|
||||
Changelog
|
||||
---------------
|
||||
|
||||
* **v2.5** : Interaction with other systems. Part 2: custom receivers
|
||||
* Finished custom receivers feature. Check [wiki](https://github.com/cihub/seelog/wiki/custom-receivers)
|
||||
* Added 'LoggerFromCustomReceiver'
|
||||
* Added 'LoggerFromWriterWithMinLevelAndFormat'
|
||||
* Added 'LoggerFromCustomReceiver'
|
||||
* Added 'LoggerFromParamConfigAs...'
|
||||
* **v2.4** : Interaction with other systems. Part 1: wrapping seelog
|
||||
* Added configurable caller stack skip logic
|
||||
* Added 'SetAdditionalStackDepth' to 'LoggerInterface'
|
||||
* **v2.3** : Rethinking 'rolling' receiver
|
||||
* Reimplemented 'rolling' receiver
|
||||
* Added 'Max rolls' feature for 'rolling' receiver with type='date'
|
||||
* Fixed 'rolling' receiver issue: renaming on Windows
|
||||
* **v2.2** : go1.0 compatibility point [go1.0 tag]
|
||||
* Fixed internal bugs
|
||||
* Added 'ANSI n [;k]' format identifier: %EscN
|
||||
* Made current release go1 compatible
|
||||
* **v2.1** : Some new features
|
||||
* Rolling receiver archiving option.
|
||||
* Added format identifier: %Line
|
||||
* Smtp: added paths to PEM files directories
|
||||
* Added format identifier: %FuncShort
|
||||
* Warn, Error and Critical methods now return an error
|
||||
* **v2.0** : Second major release. BREAKING CHANGES.
|
||||
* Support of binaries with stripped symbols
|
||||
* Added log strategy: adaptive
|
||||
* Critical message now forces Flush()
|
||||
* Added predefined formats: xml-debug, xml-debug-short, xml, xml-short, json-debug, json-debug-short, json, json-short, debug, debug-short, fast
|
||||
* Added receiver: conn (network connection writer)
|
||||
* BREAKING CHANGE: added Tracef, Debugf, Infof, etc. to satisfy the print/printf principle
|
||||
* Bug fixes
|
||||
* **v1.0** : Initial release. Features:
|
||||
* Xml config
|
||||
* Changing configurations on the fly without app restart
|
||||
* Contraints and exceptions
|
||||
* Formatting
|
||||
* Log strategies: sync, async loop, async timer
|
||||
* Receivers: buffered, console, file, rolling, smtp
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
adaptiveLoggerMaxInterval = time.Minute
|
||||
adaptiveLoggerMaxCriticalMsgCount = uint32(1000)
|
||||
)
|
||||
|
||||
// asyncAdaptiveLogger represents asynchronous adaptive logger which acts like
|
||||
// an async timer logger, but its interval depends on the current message count
|
||||
// in the queue.
|
||||
//
|
||||
// Interval = I, minInterval = m, maxInterval = M, criticalMsgCount = C, msgCount = c:
|
||||
// I = m + (C - Min(c, C)) / C * (M - m)
|
||||
type asyncAdaptiveLogger struct {
|
||||
asyncLogger
|
||||
minInterval time.Duration
|
||||
criticalMsgCount uint32
|
||||
maxInterval time.Duration
|
||||
}
|
||||
|
||||
// NewAsyncLoopLogger creates a new asynchronous adaptive logger
|
||||
func NewAsyncAdaptiveLogger(
|
||||
config *logConfig,
|
||||
minInterval time.Duration,
|
||||
maxInterval time.Duration,
|
||||
criticalMsgCount uint32) (*asyncAdaptiveLogger, error) {
|
||||
|
||||
if minInterval <= 0 {
|
||||
return nil, errors.New("async adaptive logger min interval should be > 0")
|
||||
}
|
||||
|
||||
if maxInterval > adaptiveLoggerMaxInterval {
|
||||
return nil, fmt.Errorf("async adaptive logger max interval should be <= %s",
|
||||
adaptiveLoggerMaxInterval)
|
||||
}
|
||||
|
||||
if criticalMsgCount <= 0 {
|
||||
return nil, errors.New("async adaptive logger critical msg count should be > 0")
|
||||
}
|
||||
|
||||
if criticalMsgCount > adaptiveLoggerMaxCriticalMsgCount {
|
||||
return nil, fmt.Errorf("async adaptive logger critical msg count should be <= %s",
|
||||
adaptiveLoggerMaxInterval)
|
||||
}
|
||||
|
||||
asnAdaptiveLogger := new(asyncAdaptiveLogger)
|
||||
|
||||
asnAdaptiveLogger.asyncLogger = *newAsyncLogger(config)
|
||||
asnAdaptiveLogger.minInterval = minInterval
|
||||
asnAdaptiveLogger.maxInterval = maxInterval
|
||||
asnAdaptiveLogger.criticalMsgCount = criticalMsgCount
|
||||
|
||||
go asnAdaptiveLogger.processQueue()
|
||||
|
||||
return asnAdaptiveLogger, nil
|
||||
}
|
||||
|
||||
func (asnAdaptiveLogger *asyncAdaptiveLogger) processItem() (closed bool, itemCount int) {
|
||||
asnAdaptiveLogger.queueHasElements.L.Lock()
|
||||
defer asnAdaptiveLogger.queueHasElements.L.Unlock()
|
||||
|
||||
for asnAdaptiveLogger.msgQueue.Len() == 0 && !asnAdaptiveLogger.Closed() {
|
||||
asnAdaptiveLogger.queueHasElements.Wait()
|
||||
}
|
||||
|
||||
if asnAdaptiveLogger.Closed() {
|
||||
return true, asnAdaptiveLogger.msgQueue.Len()
|
||||
}
|
||||
|
||||
asnAdaptiveLogger.processQueueElement()
|
||||
return false, asnAdaptiveLogger.msgQueue.Len() - 1
|
||||
}
|
||||
|
||||
// I = m + (C - Min(c, C)) / C * (M - m) =>
|
||||
// I = m + cDiff * mDiff,
|
||||
// cDiff = (C - Min(c, C)) / C)
|
||||
// mDiff = (M - m)
|
||||
func (asnAdaptiveLogger *asyncAdaptiveLogger) calcAdaptiveInterval(msgCount int) time.Duration {
|
||||
critCountF := float64(asnAdaptiveLogger.criticalMsgCount)
|
||||
cDiff := (critCountF - math.Min(float64(msgCount), critCountF)) / critCountF
|
||||
mDiff := float64(asnAdaptiveLogger.maxInterval - asnAdaptiveLogger.minInterval)
|
||||
|
||||
return asnAdaptiveLogger.minInterval + time.Duration(cDiff*mDiff)
|
||||
}
|
||||
|
||||
func (asnAdaptiveLogger *asyncAdaptiveLogger) processQueue() {
|
||||
for !asnAdaptiveLogger.Closed() {
|
||||
closed, itemCount := asnAdaptiveLogger.processItem()
|
||||
|
||||
if closed {
|
||||
break
|
||||
}
|
||||
|
||||
interval := asnAdaptiveLogger.calcAdaptiveInterval(itemCount)
|
||||
|
||||
<-time.After(interval)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MaxQueueSize is the critical number of messages in the queue that result in an immediate flush.
|
||||
const (
|
||||
MaxQueueSize = 10000
|
||||
)
|
||||
|
||||
type msgQueueItem struct {
|
||||
level LogLevel
|
||||
context LogContextInterface
|
||||
message fmt.Stringer
|
||||
}
|
||||
|
||||
// asyncLogger represents common data for all asynchronous loggers
|
||||
type asyncLogger struct {
|
||||
commonLogger
|
||||
msgQueue *list.List
|
||||
queueHasElements *sync.Cond
|
||||
}
|
||||
|
||||
// newAsyncLogger creates a new asynchronous logger
|
||||
func newAsyncLogger(config *logConfig) *asyncLogger {
|
||||
asnLogger := new(asyncLogger)
|
||||
|
||||
asnLogger.msgQueue = list.New()
|
||||
asnLogger.queueHasElements = sync.NewCond(new(sync.Mutex))
|
||||
|
||||
asnLogger.commonLogger = *newCommonLogger(config, asnLogger)
|
||||
|
||||
return asnLogger
|
||||
}
|
||||
|
||||
func (asnLogger *asyncLogger) innerLog(
|
||||
level LogLevel,
|
||||
context LogContextInterface,
|
||||
message fmt.Stringer) {
|
||||
|
||||
asnLogger.addMsgToQueue(level, context, message)
|
||||
}
|
||||
|
||||
func (asnLogger *asyncLogger) Close() {
|
||||
asnLogger.m.Lock()
|
||||
defer asnLogger.m.Unlock()
|
||||
|
||||
if !asnLogger.Closed() {
|
||||
asnLogger.flushQueue(true)
|
||||
asnLogger.config.RootDispatcher.Flush()
|
||||
|
||||
if err := asnLogger.config.RootDispatcher.Close(); err != nil {
|
||||
reportInternalError(err)
|
||||
}
|
||||
|
||||
asnLogger.closedM.Lock()
|
||||
asnLogger.closed = true
|
||||
asnLogger.closedM.Unlock()
|
||||
asnLogger.queueHasElements.Broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
func (asnLogger *asyncLogger) Flush() {
|
||||
asnLogger.m.Lock()
|
||||
defer asnLogger.m.Unlock()
|
||||
|
||||
if !asnLogger.Closed() {
|
||||
asnLogger.flushQueue(true)
|
||||
asnLogger.config.RootDispatcher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (asnLogger *asyncLogger) flushQueue(lockNeeded bool) {
|
||||
if lockNeeded {
|
||||
asnLogger.queueHasElements.L.Lock()
|
||||
defer asnLogger.queueHasElements.L.Unlock()
|
||||
}
|
||||
|
||||
for asnLogger.msgQueue.Len() > 0 {
|
||||
asnLogger.processQueueElement()
|
||||
}
|
||||
}
|
||||
|
||||
func (asnLogger *asyncLogger) processQueueElement() {
|
||||
if asnLogger.msgQueue.Len() > 0 {
|
||||
backElement := asnLogger.msgQueue.Front()
|
||||
msg, _ := backElement.Value.(msgQueueItem)
|
||||
asnLogger.processLogMsg(msg.level, msg.message, msg.context)
|
||||
asnLogger.msgQueue.Remove(backElement)
|
||||
}
|
||||
}
|
||||
|
||||
func (asnLogger *asyncLogger) addMsgToQueue(
|
||||
level LogLevel,
|
||||
context LogContextInterface,
|
||||
message fmt.Stringer) {
|
||||
|
||||
if !asnLogger.Closed() {
|
||||
asnLogger.queueHasElements.L.Lock()
|
||||
defer asnLogger.queueHasElements.L.Unlock()
|
||||
|
||||
if asnLogger.msgQueue.Len() >= MaxQueueSize {
|
||||
fmt.Printf("Seelog queue overflow: more than %v messages in the queue. Flushing.\n", MaxQueueSize)
|
||||
asnLogger.flushQueue(false)
|
||||
}
|
||||
|
||||
queueItem := msgQueueItem{level, context, message}
|
||||
|
||||
asnLogger.msgQueue.PushBack(queueItem)
|
||||
asnLogger.queueHasElements.Broadcast()
|
||||
} else {
|
||||
err := fmt.Errorf("queue closed! Cannot process element: %d %#v", level, message)
|
||||
reportInternalError(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
// asyncLoopLogger represents asynchronous logger which processes the log queue in
|
||||
// a 'for' loop
|
||||
type asyncLoopLogger struct {
|
||||
asyncLogger
|
||||
}
|
||||
|
||||
// NewAsyncLoopLogger creates a new asynchronous loop logger
|
||||
func NewAsyncLoopLogger(config *logConfig) *asyncLoopLogger {
|
||||
|
||||
asnLoopLogger := new(asyncLoopLogger)
|
||||
|
||||
asnLoopLogger.asyncLogger = *newAsyncLogger(config)
|
||||
|
||||
go asnLoopLogger.processQueue()
|
||||
|
||||
return asnLoopLogger
|
||||
}
|
||||
|
||||
func (asnLoopLogger *asyncLoopLogger) processItem() (closed bool) {
|
||||
asnLoopLogger.queueHasElements.L.Lock()
|
||||
defer asnLoopLogger.queueHasElements.L.Unlock()
|
||||
|
||||
for asnLoopLogger.msgQueue.Len() == 0 && !asnLoopLogger.Closed() {
|
||||
asnLoopLogger.queueHasElements.Wait()
|
||||
}
|
||||
|
||||
if asnLoopLogger.Closed() {
|
||||
return true
|
||||
}
|
||||
|
||||
asnLoopLogger.processQueueElement()
|
||||
return false
|
||||
}
|
||||
|
||||
func (asnLoopLogger *asyncLoopLogger) processQueue() {
|
||||
for !asnLoopLogger.Closed() {
|
||||
closed := asnLoopLogger.processItem()
|
||||
|
||||
if closed {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// asyncTimerLogger represents asynchronous logger which processes the log queue each
|
||||
// 'duration' nanoseconds
|
||||
type asyncTimerLogger struct {
|
||||
asyncLogger
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
// NewAsyncLoopLogger creates a new asynchronous loop logger
|
||||
func NewAsyncTimerLogger(config *logConfig, interval time.Duration) (*asyncTimerLogger, error) {
|
||||
|
||||
if interval <= 0 {
|
||||
return nil, errors.New("async logger interval should be > 0")
|
||||
}
|
||||
|
||||
asnTimerLogger := new(asyncTimerLogger)
|
||||
|
||||
asnTimerLogger.asyncLogger = *newAsyncLogger(config)
|
||||
asnTimerLogger.interval = interval
|
||||
|
||||
go asnTimerLogger.processQueue()
|
||||
|
||||
return asnTimerLogger, nil
|
||||
}
|
||||
|
||||
func (asnTimerLogger *asyncTimerLogger) processItem() (closed bool) {
|
||||
asnTimerLogger.queueHasElements.L.Lock()
|
||||
defer asnTimerLogger.queueHasElements.L.Unlock()
|
||||
|
||||
for asnTimerLogger.msgQueue.Len() == 0 && !asnTimerLogger.Closed() {
|
||||
asnTimerLogger.queueHasElements.Wait()
|
||||
}
|
||||
|
||||
if asnTimerLogger.Closed() {
|
||||
return true
|
||||
}
|
||||
|
||||
asnTimerLogger.processQueueElement()
|
||||
return false
|
||||
}
|
||||
|
||||
func (asnTimerLogger *asyncTimerLogger) processQueue() {
|
||||
for !asnTimerLogger.Closed() {
|
||||
closed := asnTimerLogger.processItem()
|
||||
|
||||
if closed {
|
||||
break
|
||||
}
|
||||
|
||||
<-time.After(asnTimerLogger.interval)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// syncLogger performs logging in the same goroutine where 'Trace/Debug/...'
|
||||
// func was called
|
||||
type syncLogger struct {
|
||||
commonLogger
|
||||
}
|
||||
|
||||
// NewSyncLogger creates a new synchronous logger
|
||||
func NewSyncLogger(config *logConfig) *syncLogger {
|
||||
syncLogger := new(syncLogger)
|
||||
|
||||
syncLogger.commonLogger = *newCommonLogger(config, syncLogger)
|
||||
|
||||
return syncLogger
|
||||
}
|
||||
|
||||
func (syncLogger *syncLogger) innerLog(
|
||||
level LogLevel,
|
||||
context LogContextInterface,
|
||||
message fmt.Stringer) {
|
||||
|
||||
syncLogger.processLogMsg(level, message, context)
|
||||
}
|
||||
|
||||
func (syncLogger *syncLogger) Close() {
|
||||
syncLogger.m.Lock()
|
||||
defer syncLogger.m.Unlock()
|
||||
|
||||
if !syncLogger.Closed() {
|
||||
if err := syncLogger.config.RootDispatcher.Close(); err != nil {
|
||||
reportInternalError(err)
|
||||
}
|
||||
syncLogger.closedM.Lock()
|
||||
syncLogger.closed = true
|
||||
syncLogger.closedM.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (syncLogger *syncLogger) Flush() {
|
||||
syncLogger.m.Lock()
|
||||
defer syncLogger.m.Unlock()
|
||||
|
||||
if !syncLogger.Closed() {
|
||||
syncLogger.config.RootDispatcher.Flush()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// LoggerFromConfigAsFile creates logger with config from file. File should contain valid seelog xml.
|
||||
func LoggerFromConfigAsFile(fileName string) (LoggerInterface, error) {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
conf, err := configFromReader(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createLoggerFromFullConfig(conf)
|
||||
}
|
||||
|
||||
// LoggerFromConfigAsBytes creates a logger with config from bytes stream. Bytes should contain valid seelog xml.
|
||||
func LoggerFromConfigAsBytes(data []byte) (LoggerInterface, error) {
|
||||
conf, err := configFromReader(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createLoggerFromFullConfig(conf)
|
||||
}
|
||||
|
||||
// LoggerFromConfigAsString creates a logger with config from a string. String should contain valid seelog xml.
|
||||
func LoggerFromConfigAsString(data string) (LoggerInterface, error) {
|
||||
return LoggerFromConfigAsBytes([]byte(data))
|
||||
}
|
||||
|
||||
// LoggerFromParamConfigAsFile does the same as LoggerFromConfigAsFile, but includes special parser options.
|
||||
// See 'CfgParseParams' comments.
|
||||
func LoggerFromParamConfigAsFile(fileName string, parserParams *CfgParseParams) (LoggerInterface, error) {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
conf, err := configFromReaderWithConfig(file, parserParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createLoggerFromFullConfig(conf)
|
||||
}
|
||||
|
||||
// LoggerFromParamConfigAsBytes does the same as LoggerFromConfigAsBytes, but includes special parser options.
|
||||
// See 'CfgParseParams' comments.
|
||||
func LoggerFromParamConfigAsBytes(data []byte, parserParams *CfgParseParams) (LoggerInterface, error) {
|
||||
conf, err := configFromReaderWithConfig(bytes.NewBuffer(data), parserParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createLoggerFromFullConfig(conf)
|
||||
}
|
||||
|
||||
// LoggerFromParamConfigAsString does the same as LoggerFromConfigAsString, but includes special parser options.
|
||||
// See 'CfgParseParams' comments.
|
||||
func LoggerFromParamConfigAsString(data string, parserParams *CfgParseParams) (LoggerInterface, error) {
|
||||
return LoggerFromParamConfigAsBytes([]byte(data), parserParams)
|
||||
}
|
||||
|
||||
// LoggerFromWriterWithMinLevel is shortcut for LoggerFromWriterWithMinLevelAndFormat(output, minLevel, DefaultMsgFormat)
|
||||
func LoggerFromWriterWithMinLevel(output io.Writer, minLevel LogLevel) (LoggerInterface, error) {
|
||||
return LoggerFromWriterWithMinLevelAndFormat(output, minLevel, DefaultMsgFormat)
|
||||
}
|
||||
|
||||
// LoggerFromWriterWithMinLevelAndFormat creates a proxy logger that uses io.Writer as the
|
||||
// receiver with minimal level = minLevel and with specified format.
|
||||
//
|
||||
// All messages with level more or equal to minLevel will be written to output and
|
||||
// formatted using the default seelog format.
|
||||
//
|
||||
// Can be called for usage with non-Seelog systems
|
||||
func LoggerFromWriterWithMinLevelAndFormat(output io.Writer, minLevel LogLevel, format string) (LoggerInterface, error) {
|
||||
constraints, err := NewMinMaxConstraints(minLevel, CriticalLvl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
formatter, err := NewFormatter(format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dispatcher, err := NewSplitDispatcher(formatter, []interface{}{output})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf, err := newFullLoggerConfig(constraints, make([]*LogLevelException, 0), dispatcher, syncloggerTypeFromString, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createLoggerFromFullConfig(conf)
|
||||
}
|
||||
|
||||
// LoggerFromXMLDecoder creates logger with config from a XML decoder starting from a specific node.
|
||||
// It should contain valid seelog xml, except for root node name.
|
||||
func LoggerFromXMLDecoder(xmlParser *xml.Decoder, rootNode xml.Token) (LoggerInterface, error) {
|
||||
conf, err := configFromXMLDecoder(xmlParser, rootNode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createLoggerFromFullConfig(conf)
|
||||
}
|
||||
|
||||
// LoggerFromCustomReceiver creates a proxy logger that uses a CustomReceiver as the
|
||||
// receiver.
|
||||
//
|
||||
// All messages will be sent to the specified custom receiver without additional
|
||||
// formatting ('%Msg' format is used).
|
||||
//
|
||||
// Check CustomReceiver, RegisterReceiver for additional info.
|
||||
//
|
||||
// NOTE 1: CustomReceiver.AfterParse is only called when a receiver is instantiated
|
||||
// by the config parser while parsing config. So, if you are not planning to use the
|
||||
// same CustomReceiver for both proxying (via LoggerFromCustomReceiver call) and
|
||||
// loading from config, just leave AfterParse implementation empty.
|
||||
//
|
||||
// NOTE 2: Unlike RegisterReceiver, LoggerFromCustomReceiver takes an already initialized
|
||||
// instance that implements CustomReceiver. So, fill it with data and perform any initialization
|
||||
// logic before calling this func and it won't be lost.
|
||||
//
|
||||
// So:
|
||||
// * RegisterReceiver takes value just to get the reflect.Type from it and then
|
||||
// instantiate it as many times as config is reloaded.
|
||||
//
|
||||
// * LoggerFromCustomReceiver takes value and uses it without modification and
|
||||
// reinstantiation, directy passing it to the dispatcher tree.
|
||||
func LoggerFromCustomReceiver(receiver CustomReceiver) (LoggerInterface, error) {
|
||||
constraints, err := NewMinMaxConstraints(TraceLvl, CriticalLvl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := NewCustomReceiverDispatcherByValue(msgonlyformatter, receiver, "user-proxy", CustomReceiverInitArgs{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dispatcher, err := NewSplitDispatcher(msgonlyformatter, []interface{}{output})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf, err := newFullLoggerConfig(constraints, make([]*LogLevelException, 0), dispatcher, syncloggerTypeFromString, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createLoggerFromFullConfig(conf)
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
errNodeMustHaveChildren = errors.New("node must have children")
|
||||
errNodeCannotHaveChildren = errors.New("node cannot have children")
|
||||
)
|
||||
|
||||
type unexpectedChildElementError struct {
|
||||
baseError
|
||||
}
|
||||
|
||||
func newUnexpectedChildElementError(msg string) *unexpectedChildElementError {
|
||||
custmsg := "Unexpected child element: " + msg
|
||||
return &unexpectedChildElementError{baseError{message: custmsg}}
|
||||
}
|
||||
|
||||
type missingArgumentError struct {
|
||||
baseError
|
||||
}
|
||||
|
||||
func newMissingArgumentError(nodeName, attrName string) *missingArgumentError {
|
||||
custmsg := "Output '" + nodeName + "' has no '" + attrName + "' attribute"
|
||||
return &missingArgumentError{baseError{message: custmsg}}
|
||||
}
|
||||
|
||||
type unexpectedAttributeError struct {
|
||||
baseError
|
||||
}
|
||||
|
||||
func newUnexpectedAttributeError(nodeName, attr string) *unexpectedAttributeError {
|
||||
custmsg := nodeName + " has unexpected attribute: " + attr
|
||||
return &unexpectedAttributeError{baseError{message: custmsg}}
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type loggerTypeFromString uint8
|
||||
|
||||
const (
|
||||
syncloggerTypeFromString = iota
|
||||
asyncLooploggerTypeFromString
|
||||
asyncTimerloggerTypeFromString
|
||||
adaptiveLoggerTypeFromString
|
||||
defaultloggerTypeFromString = asyncLooploggerTypeFromString
|
||||
)
|
||||
|
||||
const (
|
||||
syncloggerTypeFromStringStr = "sync"
|
||||
asyncloggerTypeFromStringStr = "asyncloop"
|
||||
asyncTimerloggerTypeFromStringStr = "asynctimer"
|
||||
adaptiveLoggerTypeFromStringStr = "adaptive"
|
||||
)
|
||||
|
||||
// asyncTimerLoggerData represents specific data for async timer logger
|
||||
type asyncTimerLoggerData struct {
|
||||
AsyncInterval uint32
|
||||
}
|
||||
|
||||
// adaptiveLoggerData represents specific data for adaptive timer logger
|
||||
type adaptiveLoggerData struct {
|
||||
MinInterval uint32
|
||||
MaxInterval uint32
|
||||
CriticalMsgCount uint32
|
||||
}
|
||||
|
||||
var loggerTypeToStringRepresentations = map[loggerTypeFromString]string{
|
||||
syncloggerTypeFromString: syncloggerTypeFromStringStr,
|
||||
asyncLooploggerTypeFromString: asyncloggerTypeFromStringStr,
|
||||
asyncTimerloggerTypeFromString: asyncTimerloggerTypeFromStringStr,
|
||||
adaptiveLoggerTypeFromString: adaptiveLoggerTypeFromStringStr,
|
||||
}
|
||||
|
||||
// getLoggerTypeFromString parses a string and returns a corresponding logger type, if successful.
|
||||
func getLoggerTypeFromString(logTypeString string) (level loggerTypeFromString, found bool) {
|
||||
for logType, logTypeStr := range loggerTypeToStringRepresentations {
|
||||
if logTypeStr == logTypeString {
|
||||
return logType, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// logConfig stores logging configuration. Contains messages dispatcher, allowed log level rules
|
||||
// (general constraints and exceptions)
|
||||
type logConfig struct {
|
||||
Constraints logLevelConstraints // General log level rules (>min and <max, or set of allowed levels)
|
||||
Exceptions []*LogLevelException // Exceptions to general rules for specific files or funcs
|
||||
RootDispatcher dispatcherInterface // Root of output tree
|
||||
}
|
||||
|
||||
func NewLoggerConfig(c logLevelConstraints, e []*LogLevelException, d dispatcherInterface) *logConfig {
|
||||
return &logConfig{c, e, d}
|
||||
}
|
||||
|
||||
// configForParsing is used when parsing config from file: logger type is deduced from string, params
|
||||
// need to be converted from attributes to values and passed to specific logger constructor. Also,
|
||||
// custom registered receivers and other parse params are used in this case.
|
||||
type configForParsing struct {
|
||||
logConfig
|
||||
LogType loggerTypeFromString
|
||||
LoggerData interface{}
|
||||
Params *CfgParseParams // Check cfg_parser: CfgParseParams
|
||||
}
|
||||
|
||||
func newFullLoggerConfig(
|
||||
constraints logLevelConstraints,
|
||||
exceptions []*LogLevelException,
|
||||
rootDispatcher dispatcherInterface,
|
||||
logType loggerTypeFromString,
|
||||
logData interface{},
|
||||
cfgParams *CfgParseParams) (*configForParsing, error) {
|
||||
if constraints == nil {
|
||||
return nil, errors.New("constraints can not be nil")
|
||||
}
|
||||
if rootDispatcher == nil {
|
||||
return nil, errors.New("rootDispatcher can not be nil")
|
||||
}
|
||||
|
||||
config := new(configForParsing)
|
||||
config.Constraints = constraints
|
||||
config.Exceptions = exceptions
|
||||
config.RootDispatcher = rootDispatcher
|
||||
config.LogType = logType
|
||||
config.LoggerData = logData
|
||||
config.Params = cfgParams
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// IsAllowed returns true if logging with specified log level is allowed in current context.
|
||||
// If any of exception patterns match current context, then exception constraints are applied. Otherwise,
|
||||
// the general constraints are used.
|
||||
func (config *logConfig) IsAllowed(level LogLevel, context LogContextInterface) bool {
|
||||
allowed := config.Constraints.IsAllowed(level) // General rule
|
||||
|
||||
// Exceptions:
|
||||
if context.IsValid() {
|
||||
for _, exception := range config.Exceptions {
|
||||
if exception.MatchesContext(context) {
|
||||
return exception.IsAllowed(level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allowed
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Represents constraints which form a general rule for log levels selection
|
||||
type logLevelConstraints interface {
|
||||
IsAllowed(level LogLevel) bool
|
||||
}
|
||||
|
||||
// A minMaxConstraints represents constraints which use minimal and maximal allowed log levels.
|
||||
type minMaxConstraints struct {
|
||||
min LogLevel
|
||||
max LogLevel
|
||||
}
|
||||
|
||||
// NewMinMaxConstraints creates a new minMaxConstraints struct with the specified min and max levels.
|
||||
func NewMinMaxConstraints(min LogLevel, max LogLevel) (*minMaxConstraints, error) {
|
||||
if min > max {
|
||||
return nil, fmt.Errorf("min level can't be greater than max. Got min: %d, max: %d", min, max)
|
||||
}
|
||||
if min < TraceLvl || min > CriticalLvl {
|
||||
return nil, fmt.Errorf("min level can't be less than Trace or greater than Critical. Got min: %d", min)
|
||||
}
|
||||
if max < TraceLvl || max > CriticalLvl {
|
||||
return nil, fmt.Errorf("max level can't be less than Trace or greater than Critical. Got max: %d", max)
|
||||
}
|
||||
|
||||
return &minMaxConstraints{min, max}, nil
|
||||
}
|
||||
|
||||
// IsAllowed returns true, if log level is in [min, max] range (inclusive).
|
||||
func (minMaxConstr *minMaxConstraints) IsAllowed(level LogLevel) bool {
|
||||
return level >= minMaxConstr.min && level <= minMaxConstr.max
|
||||
}
|
||||
|
||||
func (minMaxConstr *minMaxConstraints) String() string {
|
||||
return fmt.Sprintf("Min: %s. Max: %s", minMaxConstr.min, minMaxConstr.max)
|
||||
}
|
||||
|
||||
//=======================================================
|
||||
|
||||
// A listConstraints represents constraints which use allowed log levels list.
|
||||
type listConstraints struct {
|
||||
allowedLevels map[LogLevel]bool
|
||||
}
|
||||
|
||||
// NewListConstraints creates a new listConstraints struct with the specified allowed levels.
|
||||
func NewListConstraints(allowList []LogLevel) (*listConstraints, error) {
|
||||
if allowList == nil {
|
||||
return nil, errors.New("list can't be nil")
|
||||
}
|
||||
|
||||
allowLevels, err := createMapFromList(allowList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = validateOffLevel(allowLevels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &listConstraints{allowLevels}, nil
|
||||
}
|
||||
|
||||
func (listConstr *listConstraints) String() string {
|
||||
allowedList := "List: "
|
||||
|
||||
listLevel := make([]string, len(listConstr.allowedLevels))
|
||||
|
||||
var logLevel LogLevel
|
||||
i := 0
|
||||
for logLevel = TraceLvl; logLevel <= Off; logLevel++ {
|
||||
if listConstr.allowedLevels[logLevel] {
|
||||
listLevel[i] = logLevel.String()
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
allowedList += strings.Join(listLevel, ",")
|
||||
|
||||
return allowedList
|
||||
}
|
||||
|
||||
func createMapFromList(allowedList []LogLevel) (map[LogLevel]bool, error) {
|
||||
allowedLevels := make(map[LogLevel]bool, 0)
|
||||
for _, level := range allowedList {
|
||||
if level < TraceLvl || level > Off {
|
||||
return nil, fmt.Errorf("level can't be less than Trace or greater than Critical. Got level: %d", level)
|
||||
}
|
||||
allowedLevels[level] = true
|
||||
}
|
||||
return allowedLevels, nil
|
||||
}
|
||||
func validateOffLevel(allowedLevels map[LogLevel]bool) error {
|
||||
if _, ok := allowedLevels[Off]; ok && len(allowedLevels) > 1 {
|
||||
return errors.New("logLevel Off cant be mixed with other levels")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAllowed returns true, if log level is in allowed log levels list.
|
||||
// If the list contains the only item 'common.Off' then IsAllowed will always return false for any input values.
|
||||
func (listConstr *listConstraints) IsAllowed(level LogLevel) bool {
|
||||
for l := range listConstr.allowedLevels {
|
||||
if l == level && level != Off {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AllowedLevels returns allowed levels configuration as a map.
|
||||
func (listConstr *listConstraints) AllowedLevels() map[LogLevel]bool {
|
||||
return listConstr.allowedLevels
|
||||
}
|
||||
|
||||
//=======================================================
|
||||
|
||||
type offConstraints struct {
|
||||
}
|
||||
|
||||
func NewOffConstraints() (*offConstraints, error) {
|
||||
return &offConstraints{}, nil
|
||||
}
|
||||
|
||||
func (offConstr *offConstraints) IsAllowed(level LogLevel) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (offConstr *offConstraints) String() string {
|
||||
return "Off constraint"
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var workingDir = "/"
|
||||
|
||||
func init() {
|
||||
wd, err := os.Getwd()
|
||||
if err == nil {
|
||||
workingDir = filepath.ToSlash(wd) + "/"
|
||||
}
|
||||
}
|
||||
|
||||
// Represents runtime caller context.
|
||||
type LogContextInterface interface {
|
||||
// Caller's function name.
|
||||
Func() string
|
||||
// Caller's line number.
|
||||
Line() int
|
||||
// Caller's file short path (in slashed form).
|
||||
ShortPath() string
|
||||
// Caller's file full path (in slashed form).
|
||||
FullPath() string
|
||||
// Caller's file name (without path).
|
||||
FileName() string
|
||||
// True if the context is correct and may be used.
|
||||
// If false, then an error in context evaluation occurred and
|
||||
// all its other data may be corrupted.
|
||||
IsValid() bool
|
||||
// Time when log function was called.
|
||||
CallTime() time.Time
|
||||
// Custom context that can be set by calling logger.SetContext
|
||||
CustomContext() interface{}
|
||||
}
|
||||
|
||||
// Returns context of the caller
|
||||
func currentContext(custom interface{}) (LogContextInterface, error) {
|
||||
return specifyContext(1, custom)
|
||||
}
|
||||
|
||||
func extractCallerInfo(skip int) (fullPath string, shortPath string, funcName string, line int, err error) {
|
||||
pc, fp, ln, ok := runtime.Caller(skip)
|
||||
if !ok {
|
||||
err = fmt.Errorf("error during runtime.Caller")
|
||||
return
|
||||
}
|
||||
line = ln
|
||||
fullPath = fp
|
||||
if strings.HasPrefix(fp, workingDir) {
|
||||
shortPath = fp[len(workingDir):]
|
||||
} else {
|
||||
shortPath = fp
|
||||
}
|
||||
funcName = runtime.FuncForPC(pc).Name()
|
||||
if strings.HasPrefix(funcName, workingDir) {
|
||||
funcName = funcName[len(workingDir):]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Returns context of the function with placed "skip" stack frames of the caller
|
||||
// If skip == 0 then behaves like currentContext
|
||||
// Context is returned in any situation, even if error occurs. But, if an error
|
||||
// occurs, the returned context is an error context, which contains no paths
|
||||
// or names, but states that they can't be extracted.
|
||||
func specifyContext(skip int, custom interface{}) (LogContextInterface, error) {
|
||||
callTime := time.Now()
|
||||
if skip < 0 {
|
||||
err := fmt.Errorf("can not skip negative stack frames")
|
||||
return &errorContext{callTime, err}, err
|
||||
}
|
||||
fullPath, shortPath, funcName, line, err := extractCallerInfo(skip + 2)
|
||||
if err != nil {
|
||||
return &errorContext{callTime, err}, err
|
||||
}
|
||||
_, fileName := filepath.Split(fullPath)
|
||||
return &logContext{funcName, line, shortPath, fullPath, fileName, callTime, custom}, nil
|
||||
}
|
||||
|
||||
// Represents a normal runtime caller context.
|
||||
type logContext struct {
|
||||
funcName string
|
||||
line int
|
||||
shortPath string
|
||||
fullPath string
|
||||
fileName string
|
||||
callTime time.Time
|
||||
custom interface{}
|
||||
}
|
||||
|
||||
func (context *logContext) IsValid() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (context *logContext) Func() string {
|
||||
return context.funcName
|
||||
}
|
||||
|
||||
func (context *logContext) Line() int {
|
||||
return context.line
|
||||
}
|
||||
|
||||
func (context *logContext) ShortPath() string {
|
||||
return context.shortPath
|
||||
}
|
||||
|
||||
func (context *logContext) FullPath() string {
|
||||
return context.fullPath
|
||||
}
|
||||
|
||||
func (context *logContext) FileName() string {
|
||||
return context.fileName
|
||||
}
|
||||
|
||||
func (context *logContext) CallTime() time.Time {
|
||||
return context.callTime
|
||||
}
|
||||
|
||||
func (context *logContext) CustomContext() interface{} {
|
||||
return context.custom
|
||||
}
|
||||
|
||||
// Represents an error context
|
||||
type errorContext struct {
|
||||
errorTime time.Time
|
||||
err error
|
||||
}
|
||||
|
||||
func (errContext *errorContext) getErrorText(prefix string) string {
|
||||
return fmt.Sprintf("%s() error: %s", prefix, errContext.err)
|
||||
}
|
||||
|
||||
func (errContext *errorContext) IsValid() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (errContext *errorContext) Line() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (errContext *errorContext) Func() string {
|
||||
return errContext.getErrorText("Func")
|
||||
}
|
||||
|
||||
func (errContext *errorContext) ShortPath() string {
|
||||
return errContext.getErrorText("ShortPath")
|
||||
}
|
||||
|
||||
func (errContext *errorContext) FullPath() string {
|
||||
return errContext.getErrorText("FullPath")
|
||||
}
|
||||
|
||||
func (errContext *errorContext) FileName() string {
|
||||
return errContext.getErrorText("FileName")
|
||||
}
|
||||
|
||||
func (errContext *errorContext) CallTime() time.Time {
|
||||
return errContext.errorTime
|
||||
}
|
||||
|
||||
func (errContext *errorContext) CustomContext() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Used in rules creation to validate input file and func filters
|
||||
var (
|
||||
fileFormatValidator = regexp.MustCompile(`[a-zA-Z0-9\\/ _\*\.]*`)
|
||||
funcFormatValidator = regexp.MustCompile(`[a-zA-Z0-9_\*\.]*`)
|
||||
)
|
||||
|
||||
// LogLevelException represents an exceptional case used when you need some specific files or funcs to
|
||||
// override general constraints and to use their own.
|
||||
type LogLevelException struct {
|
||||
funcPatternParts []string
|
||||
filePatternParts []string
|
||||
|
||||
funcPattern string
|
||||
filePattern string
|
||||
|
||||
constraints logLevelConstraints
|
||||
}
|
||||
|
||||
// NewLogLevelException creates a new exception.
|
||||
func NewLogLevelException(funcPattern string, filePattern string, constraints logLevelConstraints) (*LogLevelException, error) {
|
||||
if constraints == nil {
|
||||
return nil, errors.New("constraints can not be nil")
|
||||
}
|
||||
|
||||
exception := new(LogLevelException)
|
||||
|
||||
err := exception.initFuncPatternParts(funcPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exception.funcPattern = strings.Join(exception.funcPatternParts, "")
|
||||
|
||||
err = exception.initFilePatternParts(filePattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exception.filePattern = strings.Join(exception.filePatternParts, "")
|
||||
|
||||
exception.constraints = constraints
|
||||
|
||||
return exception, nil
|
||||
}
|
||||
|
||||
// MatchesContext returns true if context matches the patterns of this LogLevelException
|
||||
func (logLevelEx *LogLevelException) MatchesContext(context LogContextInterface) bool {
|
||||
return logLevelEx.match(context.Func(), context.FullPath())
|
||||
}
|
||||
|
||||
// IsAllowed returns true if log level is allowed according to the constraints of this LogLevelException
|
||||
func (logLevelEx *LogLevelException) IsAllowed(level LogLevel) bool {
|
||||
return logLevelEx.constraints.IsAllowed(level)
|
||||
}
|
||||
|
||||
// FuncPattern returns the function pattern of a exception
|
||||
func (logLevelEx *LogLevelException) FuncPattern() string {
|
||||
return logLevelEx.funcPattern
|
||||
}
|
||||
|
||||
// FuncPattern returns the file pattern of a exception
|
||||
func (logLevelEx *LogLevelException) FilePattern() string {
|
||||
return logLevelEx.filePattern
|
||||
}
|
||||
|
||||
// initFuncPatternParts checks whether the func filter has a correct format and splits funcPattern on parts
|
||||
func (logLevelEx *LogLevelException) initFuncPatternParts(funcPattern string) (err error) {
|
||||
|
||||
if funcFormatValidator.FindString(funcPattern) != funcPattern {
|
||||
return errors.New("func path \"" + funcPattern + "\" contains incorrect symbols. Only a-z A-Z 0-9 _ * . allowed)")
|
||||
}
|
||||
|
||||
logLevelEx.funcPatternParts = splitPattern(funcPattern)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks whether the file filter has a correct format and splits file patterns using splitPattern.
|
||||
func (logLevelEx *LogLevelException) initFilePatternParts(filePattern string) (err error) {
|
||||
|
||||
if fileFormatValidator.FindString(filePattern) != filePattern {
|
||||
return errors.New("file path \"" + filePattern + "\" contains incorrect symbols. Only a-z A-Z 0-9 \\ / _ * . allowed)")
|
||||
}
|
||||
|
||||
logLevelEx.filePatternParts = splitPattern(filePattern)
|
||||
return err
|
||||
}
|
||||
|
||||
func (logLevelEx *LogLevelException) match(funcPath string, filePath string) bool {
|
||||
if !stringMatchesPattern(logLevelEx.funcPatternParts, funcPath) {
|
||||
return false
|
||||
}
|
||||
return stringMatchesPattern(logLevelEx.filePatternParts, filePath)
|
||||
}
|
||||
|
||||
func (logLevelEx *LogLevelException) String() string {
|
||||
str := fmt.Sprintf("Func: %s File: %s", logLevelEx.funcPattern, logLevelEx.filePattern)
|
||||
|
||||
if logLevelEx.constraints != nil {
|
||||
str += fmt.Sprintf("Constr: %s", logLevelEx.constraints)
|
||||
} else {
|
||||
str += "nil"
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// splitPattern splits pattern into strings and asterisks. Example: "ab*cde**f" -> ["ab", "*", "cde", "*", "f"]
|
||||
func splitPattern(pattern string) []string {
|
||||
var patternParts []string
|
||||
var lastChar rune
|
||||
for _, char := range pattern {
|
||||
if char == '*' {
|
||||
if lastChar != '*' {
|
||||
patternParts = append(patternParts, "*")
|
||||
}
|
||||
} else {
|
||||
if len(patternParts) != 0 && lastChar != '*' {
|
||||
patternParts[len(patternParts)-1] += string(char)
|
||||
} else {
|
||||
patternParts = append(patternParts, string(char))
|
||||
}
|
||||
}
|
||||
lastChar = char
|
||||
}
|
||||
|
||||
return patternParts
|
||||
}
|
||||
|
||||
// stringMatchesPattern check whether testString matches pattern with asterisks.
|
||||
// Standard regexp functionality is not used here because of performance issues.
|
||||
func stringMatchesPattern(patternparts []string, testString string) bool {
|
||||
if len(patternparts) == 0 {
|
||||
return len(testString) == 0
|
||||
}
|
||||
|
||||
part := patternparts[0]
|
||||
if part != "*" {
|
||||
index := strings.Index(testString, part)
|
||||
if index == 0 {
|
||||
return stringMatchesPattern(patternparts[1:], testString[len(part):])
|
||||
}
|
||||
} else {
|
||||
if len(patternparts) == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
newTestString := testString
|
||||
part = patternparts[1]
|
||||
for {
|
||||
index := strings.Index(newTestString, part)
|
||||
if index == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
newTestString = newTestString[index+len(part):]
|
||||
result := stringMatchesPattern(patternparts[2:], newTestString)
|
||||
if result {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
// flusherInterface represents all objects that have to do cleanup
|
||||
// at certain moments of time (e.g. before app shutdown to avoid data loss)
|
||||
type flusherInterface interface {
|
||||
Flush()
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
// Log level type
|
||||
type LogLevel uint8
|
||||
|
||||
// Log levels
|
||||
const (
|
||||
TraceLvl = iota
|
||||
DebugLvl
|
||||
InfoLvl
|
||||
WarnLvl
|
||||
ErrorLvl
|
||||
CriticalLvl
|
||||
Off
|
||||
)
|
||||
|
||||
// Log level string representations (used in configuration files)
|
||||
const (
|
||||
TraceStr = "trace"
|
||||
DebugStr = "debug"
|
||||
InfoStr = "info"
|
||||
WarnStr = "warn"
|
||||
ErrorStr = "error"
|
||||
CriticalStr = "critical"
|
||||
OffStr = "off"
|
||||
)
|
||||
|
||||
var levelToStringRepresentations = map[LogLevel]string{
|
||||
TraceLvl: TraceStr,
|
||||
DebugLvl: DebugStr,
|
||||
InfoLvl: InfoStr,
|
||||
WarnLvl: WarnStr,
|
||||
ErrorLvl: ErrorStr,
|
||||
CriticalLvl: CriticalStr,
|
||||
Off: OffStr,
|
||||
}
|
||||
|
||||
// LogLevelFromString parses a string and returns a corresponding log level, if sucessfull.
|
||||
func LogLevelFromString(levelStr string) (level LogLevel, found bool) {
|
||||
for lvl, lvlStr := range levelToStringRepresentations {
|
||||
if lvlStr == levelStr {
|
||||
return lvl, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// LogLevelToString returns seelog string representation for a specified level. Returns "" for invalid log levels.
|
||||
func (level LogLevel) String() string {
|
||||
levelStr, ok := levelToStringRepresentations[level]
|
||||
if ok {
|
||||
return levelStr
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
// Copyright (c) 2013 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var registeredReceivers = make(map[string]reflect.Type)
|
||||
|
||||
// RegisterReceiver records a custom receiver type, identified by a value
|
||||
// of that type (second argument), under the specified name. Registered
|
||||
// names can be used in the "name" attribute of <custom> config items.
|
||||
//
|
||||
// RegisterReceiver takes the type of the receiver argument, without taking
|
||||
// the value into the account. So do NOT enter any data to the second argument
|
||||
// and only call it like:
|
||||
// RegisterReceiver("somename", &MyReceiverType{})
|
||||
//
|
||||
// After that, when a '<custom>' config tag with this name is used,
|
||||
// a receiver of the specified type would be instantiated. Check
|
||||
// CustomReceiver comments for interface details.
|
||||
//
|
||||
// NOTE 1: RegisterReceiver fails if you attempt to register different types
|
||||
// with the same name.
|
||||
//
|
||||
// NOTE 2: RegisterReceiver registers those receivers that must be used in
|
||||
// the configuration files (<custom> items). Basically it is just the way
|
||||
// you tell seelog config parser what should it do when it meets a
|
||||
// <custom> tag with a specific name and data attributes.
|
||||
//
|
||||
// But If you are only using seelog as a proxy to an already instantiated
|
||||
// CustomReceiver (via LoggerFromCustomReceiver func), you should not call RegisterReceiver.
|
||||
func RegisterReceiver(name string, receiver CustomReceiver) {
|
||||
newType := reflect.TypeOf(reflect.ValueOf(receiver).Elem().Interface())
|
||||
if t, ok := registeredReceivers[name]; ok && t != newType {
|
||||
panic(fmt.Sprintf("duplicate types for %s: %s != %s", name, t, newType))
|
||||
}
|
||||
registeredReceivers[name] = newType
|
||||
}
|
||||
|
||||
func customReceiverByName(name string) (creceiver CustomReceiver, err error) {
|
||||
rt, ok := registeredReceivers[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("custom receiver name not registered: '%s'", name)
|
||||
}
|
||||
v, ok := reflect.New(rt).Interface().(CustomReceiver)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot instantiate receiver with name='%s'", name)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// CustomReceiverInitArgs represent arguments passed to the CustomReceiver.Init
|
||||
// func when custom receiver is being initialized.
|
||||
type CustomReceiverInitArgs struct {
|
||||
// XmlCustomAttrs represent '<custom>' xml config item attributes that
|
||||
// start with "data-". Map keys will be the attribute names without the "data-".
|
||||
// Map values will the those attribute values.
|
||||
//
|
||||
// E.g. if you have a '<custom name="somename" data-attr1="a1" data-attr2="a2"/>'
|
||||
// you will get map with 2 key-value pairs: "attr1"->"a1", "attr2"->"a2"
|
||||
//
|
||||
// Note that in custom items you can only use allowed attributes, like "name" and
|
||||
// your custom attributes, starting with "data-". Any other will lead to a
|
||||
// parsing error.
|
||||
XmlCustomAttrs map[string]string
|
||||
}
|
||||
|
||||
// CustomReceiver is the interface that external custom seelog message receivers
|
||||
// must implement in order to be able to process seelog messages. Those receivers
|
||||
// are set in the xml config file using the <custom> tag. Check receivers reference
|
||||
// wiki section on that.
|
||||
//
|
||||
// Use seelog.RegisterReceiver on the receiver type before using it.
|
||||
type CustomReceiver interface {
|
||||
// ReceiveMessage is called when the custom receiver gets seelog message from
|
||||
// a parent dispatcher.
|
||||
//
|
||||
// Message, level and context args represent all data that was included in the seelog
|
||||
// message at the time it was logged.
|
||||
//
|
||||
// The formatting is already applied to the message and depends on the config
|
||||
// like with any other receiver.
|
||||
//
|
||||
// If you would like to inform seelog of an error that happened during the handling of
|
||||
// the message, return a non-nil error. This way you'll end up seeing your error like
|
||||
// any other internal seelog error.
|
||||
ReceiveMessage(message string, level LogLevel, context LogContextInterface) error
|
||||
|
||||
// AfterParse is called immediately after your custom receiver is instantiated by
|
||||
// the xml config parser. So, if you need to do any startup logic after config parsing,
|
||||
// like opening file or allocating any resources after the receiver is instantiated, do it here.
|
||||
//
|
||||
// If this func returns a non-nil error, then the loading procedure will fail. E.g.
|
||||
// if you are loading a seelog xml config, the parser would not finish the loading
|
||||
// procedure and inform about an error like with any other config error.
|
||||
//
|
||||
// If your custom logger needs some configuration, you can use custom attributes in
|
||||
// your config. Check CustomReceiverInitArgs.XmlCustomAttrs comments.
|
||||
//
|
||||
// IMPORTANT: This func is NOT called when the LoggerFromCustomReceiver func is used
|
||||
// to create seelog proxy logger using the custom receiver. This func is only called when
|
||||
// receiver is instantiated from a config.
|
||||
AfterParse(initArgs CustomReceiverInitArgs) error
|
||||
|
||||
// Flush is called when the custom receiver gets a 'flush' directive from a
|
||||
// parent receiver. If custom receiver implements some kind of buffering or
|
||||
// queing, then the appropriate reaction on a flush message is synchronous
|
||||
// flushing of all those queues/buffers. If custom receiver doesn't have
|
||||
// such mechanisms, then flush implementation may be left empty.
|
||||
Flush()
|
||||
|
||||
// Close is called when the custom receiver gets a 'close' directive from a
|
||||
// parent receiver. This happens when a top-level seelog dispatcher is sending
|
||||
// 'close' to all child nodes and it means that current seelog logger is being closed.
|
||||
// If you need to do any cleanup after your custom receiver is done, you should do
|
||||
// it here.
|
||||
Close() error
|
||||
}
|
||||
|
||||
type customReceiverDispatcher struct {
|
||||
formatter *formatter
|
||||
innerReceiver CustomReceiver
|
||||
customReceiverName string
|
||||
usedArgs CustomReceiverInitArgs
|
||||
}
|
||||
|
||||
// NewCustomReceiverDispatcher creates a customReceiverDispatcher which dispatches data to a specific receiver created
|
||||
// using a <custom> tag in the config file.
|
||||
func NewCustomReceiverDispatcher(formatter *formatter, customReceiverName string, cArgs CustomReceiverInitArgs) (*customReceiverDispatcher, error) {
|
||||
if formatter == nil {
|
||||
return nil, errors.New("formatter cannot be nil")
|
||||
}
|
||||
if len(customReceiverName) == 0 {
|
||||
return nil, errors.New("custom receiver name cannot be empty")
|
||||
}
|
||||
|
||||
creceiver, err := customReceiverByName(customReceiverName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = creceiver.AfterParse(cArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
disp := &customReceiverDispatcher{formatter, creceiver, customReceiverName, cArgs}
|
||||
|
||||
return disp, nil
|
||||
}
|
||||
|
||||
// NewCustomReceiverDispatcherByValue is basically the same as NewCustomReceiverDispatcher, but using
|
||||
// a specific CustomReceiver value instead of instantiating a new one by type.
|
||||
func NewCustomReceiverDispatcherByValue(formatter *formatter, customReceiver CustomReceiver, name string, cArgs CustomReceiverInitArgs) (*customReceiverDispatcher, error) {
|
||||
if formatter == nil {
|
||||
return nil, errors.New("formatter cannot be nil")
|
||||
}
|
||||
if customReceiver == nil {
|
||||
return nil, errors.New("customReceiver cannot be nil")
|
||||
}
|
||||
disp := &customReceiverDispatcher{formatter, customReceiver, name, cArgs}
|
||||
|
||||
return disp, nil
|
||||
}
|
||||
|
||||
// CustomReceiver implementation. Check CustomReceiver comments.
|
||||
func (disp *customReceiverDispatcher) Dispatch(
|
||||
message string,
|
||||
level LogLevel,
|
||||
context LogContextInterface,
|
||||
errorFunc func(err error)) {
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
errorFunc(fmt.Errorf("panic in custom receiver '%s'.Dispatch: %s", reflect.TypeOf(disp.innerReceiver), err))
|
||||
}
|
||||
}()
|
||||
|
||||
err := disp.innerReceiver.ReceiveMessage(disp.formatter.Format(message, level, context), level, context)
|
||||
if err != nil {
|
||||
errorFunc(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CustomReceiver implementation. Check CustomReceiver comments.
|
||||
func (disp *customReceiverDispatcher) Flush() {
|
||||
disp.innerReceiver.Flush()
|
||||
}
|
||||
|
||||
// CustomReceiver implementation. Check CustomReceiver comments.
|
||||
func (disp *customReceiverDispatcher) Close() error {
|
||||
disp.innerReceiver.Flush()
|
||||
|
||||
err := disp.innerReceiver.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (disp *customReceiverDispatcher) String() string {
|
||||
datas := ""
|
||||
skeys := make([]string, 0, len(disp.usedArgs.XmlCustomAttrs))
|
||||
for i := range disp.usedArgs.XmlCustomAttrs {
|
||||
skeys = append(skeys, i)
|
||||
}
|
||||
sort.Strings(skeys)
|
||||
for _, key := range skeys {
|
||||
datas += fmt.Sprintf("<%s, %s> ", key, disp.usedArgs.XmlCustomAttrs[key])
|
||||
}
|
||||
|
||||
str := fmt.Sprintf("Custom receiver %s [fmt='%s'],[data='%s'],[inner='%s']\n",
|
||||
disp.customReceiverName, disp.formatter.String(), datas, disp.innerReceiver)
|
||||
|
||||
return str
|
||||
}
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// A dispatcherInterface is used to dispatch message to all underlying receivers.
|
||||
// Dispatch logic depends on given context and log level. Any errors are reported using errorFunc.
|
||||
// Also, as underlying receivers may have a state, dispatcher has a ShuttingDown method which performs
|
||||
// an immediate cleanup of all data that is stored in the receivers
|
||||
type dispatcherInterface interface {
|
||||
flusherInterface
|
||||
io.Closer
|
||||
Dispatch(message string, level LogLevel, context LogContextInterface, errorFunc func(err error))
|
||||
}
|
||||
|
||||
type dispatcher struct {
|
||||
formatter *formatter
|
||||
writers []*formattedWriter
|
||||
dispatchers []dispatcherInterface
|
||||
}
|
||||
|
||||
// Creates a dispatcher which dispatches data to a list of receivers.
|
||||
// Each receiver should be either a Dispatcher or io.Writer, otherwise an error will be returned
|
||||
func createDispatcher(formatter *formatter, receivers []interface{}) (*dispatcher, error) {
|
||||
if formatter == nil {
|
||||
return nil, errors.New("formatter cannot be nil")
|
||||
}
|
||||
if receivers == nil || len(receivers) == 0 {
|
||||
return nil, errors.New("receivers cannot be nil or empty")
|
||||
}
|
||||
|
||||
disp := &dispatcher{formatter, make([]*formattedWriter, 0), make([]dispatcherInterface, 0)}
|
||||
for _, receiver := range receivers {
|
||||
writer, ok := receiver.(*formattedWriter)
|
||||
if ok {
|
||||
disp.writers = append(disp.writers, writer)
|
||||
continue
|
||||
}
|
||||
|
||||
ioWriter, ok := receiver.(io.Writer)
|
||||
if ok {
|
||||
writer, err := NewFormattedWriter(ioWriter, disp.formatter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
disp.writers = append(disp.writers, writer)
|
||||
continue
|
||||
}
|
||||
|
||||
dispInterface, ok := receiver.(dispatcherInterface)
|
||||
if ok {
|
||||
disp.dispatchers = append(disp.dispatchers, dispInterface)
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, errors.New("method can receive either io.Writer or dispatcherInterface")
|
||||
}
|
||||
|
||||
return disp, nil
|
||||
}
|
||||
|
||||
func (disp *dispatcher) Dispatch(
|
||||
message string,
|
||||
level LogLevel,
|
||||
context LogContextInterface,
|
||||
errorFunc func(err error)) {
|
||||
|
||||
for _, writer := range disp.writers {
|
||||
err := writer.Write(message, level, context)
|
||||
if err != nil {
|
||||
errorFunc(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, dispInterface := range disp.dispatchers {
|
||||
dispInterface.Dispatch(message, level, context, errorFunc)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush goes through all underlying writers which implement flusherInterface interface
|
||||
// and closes them. Recursively performs the same action for underlying dispatchers
|
||||
func (disp *dispatcher) Flush() {
|
||||
for _, disp := range disp.Dispatchers() {
|
||||
disp.Flush()
|
||||
}
|
||||
|
||||
for _, formatWriter := range disp.Writers() {
|
||||
flusher, ok := formatWriter.Writer().(flusherInterface)
|
||||
if ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close goes through all underlying writers which implement io.Closer interface
|
||||
// and closes them. Recursively performs the same action for underlying dispatchers
|
||||
// Before closing, writers are flushed to prevent loss of any buffered data, so
|
||||
// a call to Flush() func before Close() is not necessary
|
||||
func (disp *dispatcher) Close() error {
|
||||
for _, disp := range disp.Dispatchers() {
|
||||
disp.Flush()
|
||||
err := disp.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, formatWriter := range disp.Writers() {
|
||||
flusher, ok := formatWriter.Writer().(flusherInterface)
|
||||
if ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
closer, ok := formatWriter.Writer().(io.Closer)
|
||||
if ok {
|
||||
err := closer.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (disp *dispatcher) Writers() []*formattedWriter {
|
||||
return disp.writers
|
||||
}
|
||||
|
||||
func (disp *dispatcher) Dispatchers() []dispatcherInterface {
|
||||
return disp.dispatchers
|
||||
}
|
||||
|
||||
func (disp *dispatcher) String() string {
|
||||
str := "formatter: " + disp.formatter.String() + "\n"
|
||||
|
||||
str += " ->Dispatchers:"
|
||||
|
||||
if len(disp.dispatchers) == 0 {
|
||||
str += "none\n"
|
||||
} else {
|
||||
str += "\n"
|
||||
|
||||
for _, disp := range disp.dispatchers {
|
||||
str += fmt.Sprintf(" ->%s", disp)
|
||||
}
|
||||
}
|
||||
|
||||
str += " ->Writers:"
|
||||
|
||||
if len(disp.writers) == 0 {
|
||||
str += "none\n"
|
||||
} else {
|
||||
str += "\n"
|
||||
|
||||
for _, writer := range disp.writers {
|
||||
str += fmt.Sprintf(" ->%s\n", writer)
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A filterDispatcher writes the given message to underlying receivers only if message log level
|
||||
// is in the allowed list.
|
||||
type filterDispatcher struct {
|
||||
*dispatcher
|
||||
allowList map[LogLevel]bool
|
||||
}
|
||||
|
||||
// NewFilterDispatcher creates a new filterDispatcher using a list of allowed levels.
|
||||
func NewFilterDispatcher(formatter *formatter, receivers []interface{}, allowList ...LogLevel) (*filterDispatcher, error) {
|
||||
disp, err := createDispatcher(formatter, receivers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allows := make(map[LogLevel]bool)
|
||||
for _, allowLevel := range allowList {
|
||||
allows[allowLevel] = true
|
||||
}
|
||||
|
||||
return &filterDispatcher{disp, allows}, nil
|
||||
}
|
||||
|
||||
func (filter *filterDispatcher) Dispatch(
|
||||
message string,
|
||||
level LogLevel,
|
||||
context LogContextInterface,
|
||||
errorFunc func(err error)) {
|
||||
isAllowed, ok := filter.allowList[level]
|
||||
if ok && isAllowed {
|
||||
filter.dispatcher.Dispatch(message, level, context, errorFunc)
|
||||
}
|
||||
}
|
||||
|
||||
func (filter *filterDispatcher) String() string {
|
||||
return fmt.Sprintf("filterDispatcher ->\n%s", filter.dispatcher)
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A splitDispatcher just writes the given message to underlying receivers. (Splits the message stream.)
|
||||
type splitDispatcher struct {
|
||||
*dispatcher
|
||||
}
|
||||
|
||||
func NewSplitDispatcher(formatter *formatter, receivers []interface{}) (*splitDispatcher, error) {
|
||||
disp, err := createDispatcher(formatter, receivers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &splitDispatcher{disp}, nil
|
||||
}
|
||||
|
||||
func (splitter *splitDispatcher) String() string {
|
||||
return fmt.Sprintf("splitDispatcher ->\n%s", splitter.dispatcher.String())
|
||||
}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
// Copyright (c) 2014 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
/*
|
||||
Package seelog implements logging functionality with flexible dispatching, filtering, and formatting.
|
||||
|
||||
Creation
|
||||
|
||||
To create a logger, use one of the following constructors:
|
||||
func LoggerFromConfigAsBytes
|
||||
func LoggerFromConfigAsFile
|
||||
func LoggerFromConfigAsString
|
||||
func LoggerFromWriterWithMinLevel
|
||||
func LoggerFromWriterWithMinLevelAndFormat
|
||||
func LoggerFromCustomReceiver (check https://github.com/cihub/seelog/wiki/Custom-receivers)
|
||||
Example:
|
||||
import log "github.com/cihub/seelog"
|
||||
|
||||
func main() {
|
||||
logger, err := log.LoggerFromConfigAsFile("seelog.xml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer logger.Flush()
|
||||
... use logger ...
|
||||
}
|
||||
The "defer" line is important because if you are using asynchronous logger behavior, without this line you may end up losing some
|
||||
messages when you close your application because they are processed in another non-blocking goroutine. To avoid that you
|
||||
explicitly defer flushing all messages before closing.
|
||||
|
||||
Usage
|
||||
|
||||
Logger created using one of the LoggerFrom* funcs can be used directly by calling one of the main log funcs.
|
||||
Example:
|
||||
import log "github.com/cihub/seelog"
|
||||
|
||||
func main() {
|
||||
logger, err := log.LoggerFromConfigAsFile("seelog.xml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer logger.Flush()
|
||||
logger.Trace("test")
|
||||
logger.Debugf("var = %s", "abc")
|
||||
}
|
||||
|
||||
Having loggers as variables is convenient if you are writing your own package with internal logging or if you have
|
||||
several loggers with different options.
|
||||
But for most standalone apps it is more convenient to use package level funcs and vars. There is a package level
|
||||
var 'Current' made for it. You can replace it with another logger using 'ReplaceLogger' and then use package level funcs:
|
||||
import log "github.com/cihub/seelog"
|
||||
|
||||
func main() {
|
||||
logger, err := log.LoggerFromConfigAsFile("seelog.xml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.ReplaceLogger(logger)
|
||||
defer log.Flush()
|
||||
log.Trace("test")
|
||||
log.Debugf("var = %s", "abc")
|
||||
}
|
||||
Last lines
|
||||
log.Trace("test")
|
||||
log.Debugf("var = %s", "abc")
|
||||
do the same as
|
||||
log.Current.Trace("test")
|
||||
log.Current.Debugf("var = %s", "abc")
|
||||
In this example the 'Current' logger was replaced using a 'ReplaceLogger' call and became equal to 'logger' variable created from config.
|
||||
This way you are able to use package level funcs instead of passing the logger variable.
|
||||
|
||||
Configuration
|
||||
|
||||
Main seelog point is to configure logger via config files and not the code.
|
||||
The configuration is read by LoggerFrom* funcs. These funcs read xml configuration from different sources and try
|
||||
to create a logger using it.
|
||||
|
||||
All the configuration features are covered in detail in the official wiki: https://github.com/cihub/seelog/wiki.
|
||||
There are many sections covering different aspects of seelog, but the most important for understanding configs are:
|
||||
https://github.com/cihub/seelog/wiki/Constraints-and-exceptions
|
||||
https://github.com/cihub/seelog/wiki/Dispatchers-and-receivers
|
||||
https://github.com/cihub/seelog/wiki/Formatting
|
||||
https://github.com/cihub/seelog/wiki/Logger-types
|
||||
After you understand these concepts, check the 'Reference' section on the main wiki page to get the up-to-date
|
||||
list of dispatchers, receivers, formats, and logger types.
|
||||
|
||||
Here is an example config with all these features:
|
||||
<seelog type="adaptive" mininterval="2000000" maxinterval="100000000" critmsgcount="500" minlevel="debug">
|
||||
<exceptions>
|
||||
<exception filepattern="test*" minlevel="error"/>
|
||||
</exceptions>
|
||||
<outputs formatid="all">
|
||||
<file path="all.log"/>
|
||||
<filter levels="info">
|
||||
<console formatid="fmtinfo"/>
|
||||
</filter>
|
||||
<filter levels="error,critical" formatid="fmterror">
|
||||
<console/>
|
||||
<file path="errors.log"/>
|
||||
</filter>
|
||||
</outputs>
|
||||
<formats>
|
||||
<format id="fmtinfo" format="[%Level] [%Time] %Msg%n"/>
|
||||
<format id="fmterror" format="[%LEVEL] [%Time] [%FuncShort @ %File.%Line] %Msg%n"/>
|
||||
<format id="all" format="[%Level] [%Time] [@ %File.%Line] %Msg%n"/>
|
||||
<format id="criticalemail" format="Critical error on our server!\n %Time %Date %RelFile %Func %Msg \nSent by Seelog"/>
|
||||
</formats>
|
||||
</seelog>
|
||||
This config represents a logger with adaptive timeout between log messages (check logger types reference) which
|
||||
logs to console, all.log, and errors.log depending on the log level. Its output formats also depend on log level. This logger will only
|
||||
use log level 'debug' and higher (minlevel is set) for all files with names that don't start with 'test'. For files starting with 'test'
|
||||
this logger prohibits all levels below 'error'.
|
||||
|
||||
Configuration using code
|
||||
|
||||
Although configuration using code is not recommended, it is sometimes needed and it is possible to do with seelog. Basically, what
|
||||
you need to do to get started is to create constraints, exceptions and a dispatcher tree (same as with config). Most of the New*
|
||||
functions in this package are used to provide such capabilities.
|
||||
|
||||
Here is an example of configuration in code, that demonstrates an async loop logger that logs to a simple split dispatcher with
|
||||
a console receiver using a specified format and is filtered using a top-level min-max constraints and one expection for
|
||||
the 'main.go' file. So, this is basically a demonstration of configuration of most of the features:
|
||||
|
||||
package main
|
||||
|
||||
import log "github.com/cihub/seelog"
|
||||
|
||||
func main() {
|
||||
defer log.Flush()
|
||||
log.Info("Hello from Seelog!")
|
||||
|
||||
consoleWriter, _ := log.NewConsoleWriter()
|
||||
formatter, _ := log.NewFormatter("%Level %Msg %File%n")
|
||||
root, _ := log.NewSplitDispatcher(formatter, []interface{}{consoleWriter})
|
||||
constraints, _ := log.NewMinMaxConstraints(log.TraceLvl, log.CriticalLvl)
|
||||
specificConstraints, _ := log.NewListConstraints([]log.LogLevel{log.InfoLvl, log.ErrorLvl})
|
||||
ex, _ := log.NewLogLevelException("*", "*main.go", specificConstraints)
|
||||
exceptions := []*log.LogLevelException{ex}
|
||||
|
||||
logger := log.NewAsyncLoopLogger(log.NewLoggerConfig(constraints, exceptions, root))
|
||||
log.ReplaceLogger(logger)
|
||||
|
||||
log.Trace("This should not be seen")
|
||||
log.Debug("This should not be seen")
|
||||
log.Info("Test")
|
||||
log.Error("Test2")
|
||||
}
|
||||
|
||||
Examples
|
||||
|
||||
To learn seelog features faster you should check the examples package: https://github.com/cihub/seelog-examples
|
||||
It contains many example configs and usecases.
|
||||
*/
|
||||
package seelog
|
||||
|
|
@ -1,461 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// FormatterSymbol is a special symbol used in config files to mark special format aliases.
|
||||
const (
|
||||
FormatterSymbol = '%'
|
||||
)
|
||||
|
||||
const (
|
||||
formatterParameterStart = '('
|
||||
formatterParameterEnd = ')'
|
||||
)
|
||||
|
||||
// Time and date formats used for %Date and %Time aliases.
|
||||
const (
|
||||
DateDefaultFormat = "2006-01-02"
|
||||
TimeFormat = "15:04:05"
|
||||
)
|
||||
|
||||
var DefaultMsgFormat = "%Ns [%Level] %Msg%n"
|
||||
|
||||
var (
|
||||
DefaultFormatter *formatter
|
||||
msgonlyformatter *formatter
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
if DefaultFormatter, err = NewFormatter(DefaultMsgFormat); err != nil {
|
||||
reportInternalError(fmt.Errorf("error during creating DefaultFormatter: %s", err))
|
||||
}
|
||||
if msgonlyformatter, err = NewFormatter("%Msg"); err != nil {
|
||||
reportInternalError(fmt.Errorf("error during creating msgonlyformatter: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
// FormatterFunc represents one formatter object that starts with '%' sign in the 'format' attribute
|
||||
// of the 'format' config item. These special symbols are replaced with context values or special
|
||||
// strings when message is written to byte receiver.
|
||||
//
|
||||
// Check https://github.com/cihub/seelog/wiki/Formatting for details.
|
||||
// Full list (with descriptions) of formatters: https://github.com/cihub/seelog/wiki/Format-reference
|
||||
//
|
||||
// FormatterFunc takes raw log message, level, log context and returns a string, number (of any type) or any object
|
||||
// that can be evaluated as string.
|
||||
type FormatterFunc func(message string, level LogLevel, context LogContextInterface) interface{}
|
||||
|
||||
// FormatterFuncCreator is a factory of FormatterFunc objects. It is used to generate parameterized
|
||||
// formatters (such as %Date or %EscM) and custom user formatters.
|
||||
type FormatterFuncCreator func(param string) FormatterFunc
|
||||
|
||||
var formatterFuncs = map[string]FormatterFunc{
|
||||
"Level": formatterLevel,
|
||||
"Lev": formatterLev,
|
||||
"LEVEL": formatterLEVEL,
|
||||
"LEV": formatterLEV,
|
||||
"l": formatterl,
|
||||
"Msg": formatterMsg,
|
||||
"FullPath": formatterFullPath,
|
||||
"File": formatterFile,
|
||||
"RelFile": formatterRelFile,
|
||||
"Func": FormatterFunction,
|
||||
"FuncShort": FormatterFunctionShort,
|
||||
"Line": formatterLine,
|
||||
"Time": formatterTime,
|
||||
"UTCTime": formatterUTCTime,
|
||||
"Ns": formatterNs,
|
||||
"UTCNs": formatterUTCNs,
|
||||
"n": formattern,
|
||||
"t": formattert,
|
||||
}
|
||||
|
||||
var formatterFuncsParameterized = map[string]FormatterFuncCreator{
|
||||
"Date": createDateTimeFormatterFunc,
|
||||
"UTCDate": createUTCDateTimeFormatterFunc,
|
||||
"EscM": createANSIEscapeFunc,
|
||||
}
|
||||
|
||||
func errorAliasReserved(name string) error {
|
||||
return fmt.Errorf("cannot use '%s' as custom formatter name. Name is reserved", name)
|
||||
}
|
||||
|
||||
// RegisterCustomFormatter registers a new custom formatter factory with a given name. If returned error is nil,
|
||||
// then this name (prepended by '%' symbol) can be used in 'format' attributes in configuration and
|
||||
// it will be treated like the standard parameterized formatter identifiers.
|
||||
//
|
||||
// RegisterCustomFormatter needs to be called before creating a logger for it to take effect. The general recommendation
|
||||
// is to call it once in 'init' func of your application or any initializer func.
|
||||
//
|
||||
// For usage examples, check https://github.com/cihub/seelog/wiki/Custom-formatters.
|
||||
//
|
||||
// Name must only consist of letters (unicode.IsLetter).
|
||||
//
|
||||
// Name must not be one of the already registered standard formatter names
|
||||
// (https://github.com/cihub/seelog/wiki/Format-reference) and previously registered
|
||||
// custom format names. To avoid any potential name conflicts (in future releases), it is recommended
|
||||
// to start your custom formatter name with a namespace (e.g. 'MyCompanySomething') or a 'Custom' keyword.
|
||||
func RegisterCustomFormatter(name string, creator FormatterFuncCreator) error {
|
||||
if _, ok := formatterFuncs[name]; ok {
|
||||
return errorAliasReserved(name)
|
||||
}
|
||||
if _, ok := formatterFuncsParameterized[name]; ok {
|
||||
return errorAliasReserved(name)
|
||||
}
|
||||
formatterFuncsParameterized[name] = creator
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatter is used to write messages in a specific format, inserting such additional data
|
||||
// as log level, date/time, etc.
|
||||
type formatter struct {
|
||||
fmtStringOriginal string
|
||||
fmtString string
|
||||
formatterFuncs []FormatterFunc
|
||||
}
|
||||
|
||||
// NewFormatter creates a new formatter using a format string
|
||||
func NewFormatter(formatString string) (*formatter, error) {
|
||||
fmtr := new(formatter)
|
||||
fmtr.fmtStringOriginal = formatString
|
||||
if err := buildFormatterFuncs(fmtr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fmtr, nil
|
||||
}
|
||||
|
||||
func buildFormatterFuncs(formatter *formatter) error {
|
||||
var (
|
||||
fsbuf = new(bytes.Buffer)
|
||||
fsolm1 = len(formatter.fmtStringOriginal) - 1
|
||||
)
|
||||
for i := 0; i <= fsolm1; i++ {
|
||||
if char := formatter.fmtStringOriginal[i]; char != FormatterSymbol {
|
||||
fsbuf.WriteByte(char)
|
||||
continue
|
||||
}
|
||||
// Check if the index is at the end of the string.
|
||||
if i == fsolm1 {
|
||||
return fmt.Errorf("format error: %c cannot be last symbol", FormatterSymbol)
|
||||
}
|
||||
// Check if the formatter symbol is doubled and skip it as nonmatching.
|
||||
if formatter.fmtStringOriginal[i+1] == FormatterSymbol {
|
||||
fsbuf.WriteRune(FormatterSymbol)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
function, ni, err := formatter.extractFormatterFunc(i + 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Append formatting string "%v".
|
||||
fsbuf.Write([]byte{37, 118})
|
||||
i = ni
|
||||
formatter.formatterFuncs = append(formatter.formatterFuncs, function)
|
||||
}
|
||||
formatter.fmtString = fsbuf.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (formatter *formatter) extractFormatterFunc(index int) (FormatterFunc, int, error) {
|
||||
letterSequence := formatter.extractLetterSequence(index)
|
||||
if len(letterSequence) == 0 {
|
||||
return nil, 0, fmt.Errorf("format error: lack of formatter after %c at %d", FormatterSymbol, index)
|
||||
}
|
||||
|
||||
function, formatterLength, ok := formatter.findFormatterFunc(letterSequence)
|
||||
if ok {
|
||||
return function, index + formatterLength - 1, nil
|
||||
}
|
||||
|
||||
function, formatterLength, ok, err := formatter.findFormatterFuncParametrized(letterSequence, index)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if ok {
|
||||
return function, index + formatterLength - 1, nil
|
||||
}
|
||||
|
||||
return nil, 0, errors.New("format error: unrecognized formatter at " + strconv.Itoa(index) + ": " + letterSequence)
|
||||
}
|
||||
|
||||
func (formatter *formatter) extractLetterSequence(index int) string {
|
||||
letters := ""
|
||||
|
||||
bytesToParse := []byte(formatter.fmtStringOriginal[index:])
|
||||
runeCount := utf8.RuneCount(bytesToParse)
|
||||
for i := 0; i < runeCount; i++ {
|
||||
rune, runeSize := utf8.DecodeRune(bytesToParse)
|
||||
bytesToParse = bytesToParse[runeSize:]
|
||||
|
||||
if unicode.IsLetter(rune) {
|
||||
letters += string(rune)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return letters
|
||||
}
|
||||
|
||||
func (formatter *formatter) findFormatterFunc(letters string) (FormatterFunc, int, bool) {
|
||||
currentVerb := letters
|
||||
for i := 0; i < len(letters); i++ {
|
||||
function, ok := formatterFuncs[currentVerb]
|
||||
if ok {
|
||||
return function, len(currentVerb), ok
|
||||
}
|
||||
currentVerb = currentVerb[:len(currentVerb)-1]
|
||||
}
|
||||
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
func (formatter *formatter) findFormatterFuncParametrized(letters string, lettersStartIndex int) (FormatterFunc, int, bool, error) {
|
||||
currentVerb := letters
|
||||
for i := 0; i < len(letters); i++ {
|
||||
functionCreator, ok := formatterFuncsParameterized[currentVerb]
|
||||
if ok {
|
||||
parameter := ""
|
||||
parameterLen := 0
|
||||
isVerbEqualsLetters := i == 0 // if not, then letter goes after formatter, and formatter is parameterless
|
||||
if isVerbEqualsLetters {
|
||||
userParameter := ""
|
||||
var err error
|
||||
userParameter, parameterLen, ok, err = formatter.findparameter(lettersStartIndex + len(currentVerb))
|
||||
if ok {
|
||||
parameter = userParameter
|
||||
} else if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
}
|
||||
|
||||
return functionCreator(parameter), len(currentVerb) + parameterLen, true, nil
|
||||
}
|
||||
|
||||
currentVerb = currentVerb[:len(currentVerb)-1]
|
||||
}
|
||||
|
||||
return nil, 0, false, nil
|
||||
}
|
||||
|
||||
func (formatter *formatter) findparameter(startIndex int) (string, int, bool, error) {
|
||||
if len(formatter.fmtStringOriginal) == startIndex || formatter.fmtStringOriginal[startIndex] != formatterParameterStart {
|
||||
return "", 0, false, nil
|
||||
}
|
||||
|
||||
endIndex := strings.Index(formatter.fmtStringOriginal[startIndex:], string(formatterParameterEnd))
|
||||
if endIndex == -1 {
|
||||
return "", 0, false, fmt.Errorf("Unmatched parenthesis or invalid parameter at %d: %s",
|
||||
startIndex, formatter.fmtStringOriginal[startIndex:])
|
||||
}
|
||||
endIndex += startIndex
|
||||
|
||||
length := endIndex - startIndex + 1
|
||||
|
||||
return formatter.fmtStringOriginal[startIndex+1 : endIndex], length, true, nil
|
||||
}
|
||||
|
||||
// Format processes a message with special formatters, log level, and context. Returns formatted string
|
||||
// with all formatter identifiers changed to appropriate values.
|
||||
func (formatter *formatter) Format(message string, level LogLevel, context LogContextInterface) string {
|
||||
if len(formatter.formatterFuncs) == 0 {
|
||||
return formatter.fmtString
|
||||
}
|
||||
|
||||
params := make([]interface{}, len(formatter.formatterFuncs))
|
||||
for i, function := range formatter.formatterFuncs {
|
||||
params[i] = function(message, level, context)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(formatter.fmtString, params...)
|
||||
}
|
||||
|
||||
func (formatter *formatter) String() string {
|
||||
return formatter.fmtStringOriginal
|
||||
}
|
||||
|
||||
//=====================================================
|
||||
|
||||
const (
|
||||
wrongLogLevel = "WRONG_LOGLEVEL"
|
||||
wrongEscapeCode = "WRONG_ESCAPE"
|
||||
)
|
||||
|
||||
var levelToString = map[LogLevel]string{
|
||||
TraceLvl: "Trace",
|
||||
DebugLvl: "Debug",
|
||||
InfoLvl: "Info",
|
||||
WarnLvl: "Warn",
|
||||
ErrorLvl: "Error",
|
||||
CriticalLvl: "Critical",
|
||||
Off: "Off",
|
||||
}
|
||||
|
||||
var levelToShortString = map[LogLevel]string{
|
||||
TraceLvl: "Trc",
|
||||
DebugLvl: "Dbg",
|
||||
InfoLvl: "Inf",
|
||||
WarnLvl: "Wrn",
|
||||
ErrorLvl: "Err",
|
||||
CriticalLvl: "Crt",
|
||||
Off: "Off",
|
||||
}
|
||||
|
||||
var levelToShortestString = map[LogLevel]string{
|
||||
TraceLvl: "t",
|
||||
DebugLvl: "d",
|
||||
InfoLvl: "i",
|
||||
WarnLvl: "w",
|
||||
ErrorLvl: "e",
|
||||
CriticalLvl: "c",
|
||||
Off: "o",
|
||||
}
|
||||
|
||||
func formatterLevel(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
levelStr, ok := levelToString[level]
|
||||
if !ok {
|
||||
return wrongLogLevel
|
||||
}
|
||||
return levelStr
|
||||
}
|
||||
|
||||
func formatterLev(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
levelStr, ok := levelToShortString[level]
|
||||
if !ok {
|
||||
return wrongLogLevel
|
||||
}
|
||||
return levelStr
|
||||
}
|
||||
|
||||
func formatterLEVEL(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return strings.ToTitle(formatterLevel(message, level, context).(string))
|
||||
}
|
||||
|
||||
func formatterLEV(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return strings.ToTitle(formatterLev(message, level, context).(string))
|
||||
}
|
||||
|
||||
func formatterl(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
levelStr, ok := levelToShortestString[level]
|
||||
if !ok {
|
||||
return wrongLogLevel
|
||||
}
|
||||
return levelStr
|
||||
}
|
||||
|
||||
func formatterMsg(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return message
|
||||
}
|
||||
|
||||
func formatterFullPath(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.FullPath()
|
||||
}
|
||||
|
||||
func formatterFile(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.FileName()
|
||||
}
|
||||
|
||||
func formatterRelFile(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.ShortPath()
|
||||
}
|
||||
|
||||
func FormatterFunction(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.Func()
|
||||
}
|
||||
|
||||
func FormatterFunctionShort(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
f := context.Func()
|
||||
spl := strings.Split(f, ".")
|
||||
return spl[len(spl)-1]
|
||||
}
|
||||
|
||||
func formatterLine(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.Line()
|
||||
}
|
||||
|
||||
func formatterTime(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.CallTime().Format(TimeFormat)
|
||||
}
|
||||
|
||||
func formatterUTCTime(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.CallTime().UTC().Format(TimeFormat)
|
||||
}
|
||||
|
||||
func formatterNs(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.CallTime().UnixNano()
|
||||
}
|
||||
|
||||
func formatterUTCNs(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.CallTime().UTC().UnixNano()
|
||||
}
|
||||
|
||||
func formattern(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return "\n"
|
||||
}
|
||||
|
||||
func formattert(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return "\t"
|
||||
}
|
||||
|
||||
func createDateTimeFormatterFunc(dateTimeFormat string) FormatterFunc {
|
||||
format := dateTimeFormat
|
||||
if format == "" {
|
||||
format = DateDefaultFormat
|
||||
}
|
||||
return func(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.CallTime().Format(format)
|
||||
}
|
||||
}
|
||||
|
||||
func createUTCDateTimeFormatterFunc(dateTimeFormat string) FormatterFunc {
|
||||
format := dateTimeFormat
|
||||
if format == "" {
|
||||
format = DateDefaultFormat
|
||||
}
|
||||
return func(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
return context.CallTime().UTC().Format(format)
|
||||
}
|
||||
}
|
||||
|
||||
func createANSIEscapeFunc(escapeCodeString string) FormatterFunc {
|
||||
return func(message string, level LogLevel, context LogContextInterface) interface{} {
|
||||
if len(escapeCodeString) == 0 {
|
||||
return wrongEscapeCode
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%c[%sm", 0x1B, escapeCodeString)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package seelog
|
||||
|
||||
// Base struct for custom errors.
|
||||
type baseError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (be baseError) Error() string {
|
||||
return be.message
|
||||
}
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
package seelog
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// File and directory permitions.
|
||||
const (
|
||||
defaultFilePermissions = 0666
|
||||
defaultDirectoryPermissions = 0767
|
||||
)
|
||||
|
||||
const (
|
||||
// Max number of directories can be read asynchronously.
|
||||
maxDirNumberReadAsync = 1000
|
||||
)
|
||||
|
||||
type cannotOpenFileError struct {
|
||||
baseError
|
||||
}
|
||||
|
||||
func newCannotOpenFileError(fname string) *cannotOpenFileError {
|
||||
return &cannotOpenFileError{baseError{message: "Cannot open file: " + fname}}
|
||||
}
|
||||
|
||||
type notDirectoryError struct {
|
||||
baseError
|
||||
}
|
||||
|
||||
func newNotDirectoryError(dname string) *notDirectoryError {
|
||||
return ¬DirectoryError{baseError{message: dname + " is not directory"}}
|
||||
}
|
||||
|
||||
// fileFilter is a filtering criteria function for '*os.File'.
|
||||
// Must return 'false' to set aside the given file.
|
||||
type fileFilter func(os.FileInfo, *os.File) bool
|
||||
|
||||
// filePathFilter is a filtering creteria function for file path.
|
||||
// Must return 'false' to set aside the given file.
|
||||
type filePathFilter func(filePath string) bool
|
||||
|
||||
// GetSubdirNames returns a list of directories found in
|
||||
// the given one with dirPath.
|
||||
func getSubdirNames(dirPath string) ([]string, error) {
|
||||
fi, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return nil, newNotDirectoryError(dirPath)
|
||||
}
|
||||
dd, err := os.Open(dirPath)
|
||||
// Cannot open file.
|
||||
if err != nil {
|
||||
if dd != nil {
|
||||
dd.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer dd.Close()
|
||||
// TODO: Improve performance by buffering reading.
|
||||
allEntities, err := dd.Readdir(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subDirs := []string{}
|
||||
for _, entity := range allEntities {
|
||||
if entity.IsDir() {
|
||||
subDirs = append(subDirs, entity.Name())
|
||||
}
|
||||
}
|
||||
return subDirs, nil
|
||||
}
|
||||
|
||||
// getSubdirAbsPaths recursively visit all the subdirectories
|
||||
// starting from the given directory and returns absolute paths for them.
|
||||
func getAllSubdirAbsPaths(dirPath string) (res []string, err error) {
|
||||
dps, err := getSubdirAbsPaths(dirPath)
|
||||
if err != nil {
|
||||
res = []string{}
|
||||
return
|
||||
}
|
||||
res = append(res, dps...)
|
||||
for _, dp := range dps {
|
||||
sdps, err := getAllSubdirAbsPaths(dp)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
res = append(res, sdps...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getSubdirAbsPaths supplies absolute paths for all subdirectiries in a given directory.
|
||||
// Input: (I1) dirPath - absolute path of a directory in question.
|
||||
// Out: (O1) - slice of subdir asbolute paths; (O2) - error of the operation.
|
||||
// Remark: If error (O2) is non-nil then (O1) is nil and vice versa.
|
||||
func getSubdirAbsPaths(dirPath string) ([]string, error) {
|
||||
sdns, err := getSubdirNames(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsdns := []string{}
|
||||
for _, sdn := range sdns {
|
||||
rsdns = append(rsdns, filepath.Join(dirPath, sdn))
|
||||
}
|
||||
return rsdns, nil
|
||||
}
|
||||
|
||||
// getOpenFilesInDir supplies a slice of os.File pointers to files located in the directory.
|
||||
// Remark: Ignores files for which fileFilter returns false
|
||||
func getOpenFilesInDir(dirPath string, fFilter fileFilter) ([]*os.File, error) {
|
||||
dfi, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return nil, newCannotOpenFileError("Cannot open directory " + dirPath)
|
||||
}
|
||||
defer dfi.Close()
|
||||
// Size of read buffer (i.e. chunk of items read at a time).
|
||||
rbs := 64
|
||||
resFiles := []*os.File{}
|
||||
L:
|
||||
for {
|
||||
// Read directory entities by reasonable chuncks
|
||||
// to prevent overflows on big number of files.
|
||||
fis, e := dfi.Readdir(rbs)
|
||||
switch e {
|
||||
// It's OK.
|
||||
case nil:
|
||||
// Do nothing, just continue cycle.
|
||||
case io.EOF:
|
||||
break L
|
||||
// Something went wrong.
|
||||
default:
|
||||
return nil, e
|
||||
}
|
||||
// THINK: Maybe, use async running.
|
||||
for _, fi := range fis {
|
||||
// NB: On Linux this could be a problem as
|
||||
// there are lots of file types available.
|
||||
if !fi.IsDir() {
|
||||
f, e := os.Open(filepath.Join(dirPath, fi.Name()))
|
||||
if e != nil {
|
||||
if f != nil {
|
||||
f.Close()
|
||||
}
|
||||
// THINK: Add nil as indicator that a problem occurred.
|
||||
resFiles = append(resFiles, nil)
|
||||
continue
|
||||
}
|
||||
// Check filter condition.
|
||||
if fFilter != nil && !fFilter(fi, f) {
|
||||
continue
|
||||
}
|
||||
resFiles = append(resFiles, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
return resFiles, nil
|
||||
}
|
||||
|
||||
func isRegular(m os.FileMode) bool {
|
||||
return m&os.ModeType == 0
|
||||
}
|
||||
|
||||
// getDirFilePaths return full paths of the files located in the directory.
|
||||
// Remark: Ignores files for which fileFilter returns false.
|
||||
func getDirFilePaths(dirPath string, fpFilter filePathFilter, pathIsName bool) ([]string, error) {
|
||||
dfi, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return nil, newCannotOpenFileError("Cannot open directory " + dirPath)
|
||||
}
|
||||
defer dfi.Close()
|
||||
|
||||
var absDirPath string
|
||||
if !filepath.IsAbs(dirPath) {
|
||||
absDirPath, err = filepath.Abs(dirPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get absolute path of directory: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
absDirPath = dirPath
|
||||
}
|
||||
|
||||
// TODO: check if dirPath is really directory.
|
||||
// Size of read buffer (i.e. chunk of items read at a time).
|
||||
rbs := 2 << 5
|
||||
filePaths := []string{}
|
||||
|
||||
var fp string
|
||||
L:
|
||||
for {
|
||||
// Read directory entities by reasonable chuncks
|
||||
// to prevent overflows on big number of files.
|
||||
fis, e := dfi.Readdir(rbs)
|
||||
switch e {
|
||||
// It's OK.
|
||||
case nil:
|
||||
// Do nothing, just continue cycle.
|
||||
case io.EOF:
|
||||
break L
|
||||
// Indicate that something went wrong.
|
||||
default:
|
||||
return nil, e
|
||||
}
|
||||
// THINK: Maybe, use async running.
|
||||
for _, fi := range fis {
|
||||
// NB: Should work on every Windows and non-Windows OS.
|
||||
if isRegular(fi.Mode()) {
|
||||
if pathIsName {
|
||||
fp = fi.Name()
|
||||
} else {
|
||||
// Build full path of a file.
|
||||
fp = filepath.Join(absDirPath, fi.Name())
|
||||
}
|
||||
// Check filter condition.
|
||||
if fpFilter != nil && !fpFilter(fp) {
|
||||
continue
|
||||
}
|
||||
filePaths = append(filePaths, fp)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filePaths, nil
|
||||
}
|
||||
|
||||
// getOpenFilesByDirectoryAsync runs async reading directories 'dirPaths' and inserts pairs
|
||||
// in map 'filesInDirMap': Key - directory name, value - *os.File slice.
|
||||
func getOpenFilesByDirectoryAsync(
|
||||
dirPaths []string,
|
||||
fFilter fileFilter,
|
||||
filesInDirMap map[string][]*os.File,
|
||||
) error {
|
||||
n := len(dirPaths)
|
||||
if n > maxDirNumberReadAsync {
|
||||
return fmt.Errorf("number of input directories to be read exceeded max value %d", maxDirNumberReadAsync)
|
||||
}
|
||||
type filesInDirResult struct {
|
||||
DirName string
|
||||
Files []*os.File
|
||||
Error error
|
||||
}
|
||||
dirFilesChan := make(chan *filesInDirResult, n)
|
||||
var wg sync.WaitGroup
|
||||
// Register n goroutines which are going to do work.
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
// Launch asynchronously the piece of work.
|
||||
go func(dirPath string) {
|
||||
fs, e := getOpenFilesInDir(dirPath, fFilter)
|
||||
dirFilesChan <- &filesInDirResult{filepath.Base(dirPath), fs, e}
|
||||
// Mark the current goroutine as finished (work is done).
|
||||
wg.Done()
|
||||
}(dirPaths[i])
|
||||
}
|
||||
// Wait for all goroutines to finish their work.
|
||||
wg.Wait()
|
||||
// Close the error channel to let for-range clause
|
||||
// get all the buffered values without blocking and quit in the end.
|
||||
close(dirFilesChan)
|
||||
for fidr := range dirFilesChan {
|
||||
if fidr.Error == nil {
|
||||
// THINK: What will happen if the key is already present?
|
||||
filesInDirMap[fidr.DirName] = fidr.Files
|
||||
} else {
|
||||
return fidr.Error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(sf *os.File, dst string) (int64, error) {
|
||||
df, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer df.Close()
|
||||
return io.Copy(df, sf)
|
||||
}
|
||||
|
||||
// fileExists return flag whether a given file exists
|
||||
// and operation error if an unclassified failure occurs.
|
||||
func fileExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// createDirectory makes directory with a given name
|
||||
// making all parent directories if necessary.
|
||||
func createDirectory(dirPath string) error {
|
||||
var dPath string
|
||||
var err error
|
||||
if !filepath.IsAbs(dirPath) {
|
||||
dPath, err = filepath.Abs(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dPath = dirPath
|
||||
}
|
||||
exists, err := fileExists(dPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
return os.MkdirAll(dPath, os.ModeDir)
|
||||
}
|
||||
|
||||
// tryRemoveFile gives a try removing the file
|
||||
// only ignoring an error when the file does not exist.
|
||||
func tryRemoveFile(filePath string) (err error) {
|
||||
err = os.Remove(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Unzips a specified zip file. Returns filename->filebytes map.
|
||||
func unzip(archiveName string) (map[string][]byte, error) {
|
||||
// Open a zip archive for reading.
|
||||
r, err := zip.OpenReader(archiveName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Files to be added to archive
|
||||
// map file name to contents
|
||||
files := make(map[string][]byte)
|
||||
|
||||
// Iterate through the files in the archive,
|
||||
// printing some of their contents.
|
||||
for _, f := range r.File {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bts, err := ioutil.ReadAll(rc)
|
||||
rcErr := rc.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rcErr != nil {
|
||||
return nil, rcErr
|
||||
}
|
||||
|
||||
files[f.Name] = bts
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// Creates a zip file with the specified file names and byte contents.
|
||||
func createZip(archiveName string, files map[string][]byte) error {
|
||||
// Create a buffer to write our archive to.
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
// Create a new zip archive.
|
||||
w := zip.NewWriter(buf)
|
||||
|
||||
// Write files
|
||||
for fpath, fcont := range files {
|
||||
f, err := w.Create(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.Write([]byte(fcont))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to check the error on Close.
|
||||
err := w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(archiveName, buf.Bytes(), defaultFilePermissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type xmlNode struct {
|
||||
name string
|
||||
attributes map[string]string
|
||||
children []*xmlNode
|
||||
value string
|
||||
}
|
||||
|
||||
func newNode() *xmlNode {
|
||||
node := new(xmlNode)
|
||||
node.children = make([]*xmlNode, 0)
|
||||
node.attributes = make(map[string]string)
|
||||
return node
|
||||
}
|
||||
|
||||
func (node *xmlNode) String() string {
|
||||
str := fmt.Sprintf("<%s", node.name)
|
||||
|
||||
for attrName, attrVal := range node.attributes {
|
||||
str += fmt.Sprintf(" %s=\"%s\"", attrName, attrVal)
|
||||
}
|
||||
|
||||
str += ">"
|
||||
str += node.value
|
||||
|
||||
if len(node.children) != 0 {
|
||||
for _, child := range node.children {
|
||||
str += fmt.Sprintf("%s", child)
|
||||
}
|
||||
}
|
||||
|
||||
str += fmt.Sprintf("</%s>", node.name)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (node *xmlNode) unmarshal(startEl xml.StartElement) error {
|
||||
node.name = startEl.Name.Local
|
||||
|
||||
for _, v := range startEl.Attr {
|
||||
_, alreadyExists := node.attributes[v.Name.Local]
|
||||
if alreadyExists {
|
||||
return errors.New("tag '" + node.name + "' has duplicated attribute: '" + v.Name.Local + "'")
|
||||
}
|
||||
node.attributes[v.Name.Local] = v.Value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (node *xmlNode) add(child *xmlNode) {
|
||||
if node.children == nil {
|
||||
node.children = make([]*xmlNode, 0)
|
||||
}
|
||||
|
||||
node.children = append(node.children, child)
|
||||
}
|
||||
|
||||
func (node *xmlNode) hasChildren() bool {
|
||||
return node.children != nil && len(node.children) > 0
|
||||
}
|
||||
|
||||
//=============================================
|
||||
|
||||
func unmarshalConfig(reader io.Reader) (*xmlNode, error) {
|
||||
xmlParser := xml.NewDecoder(reader)
|
||||
|
||||
config, err := unmarshalNode(xmlParser, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, errors.New("xml has no content")
|
||||
}
|
||||
|
||||
nextConfigEntry, err := unmarshalNode(xmlParser, nil)
|
||||
if nextConfigEntry != nil {
|
||||
return nil, errors.New("xml contains more than one root element")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func unmarshalNode(xmlParser *xml.Decoder, curToken xml.Token) (node *xmlNode, err error) {
|
||||
firstLoop := true
|
||||
for {
|
||||
var tok xml.Token
|
||||
if firstLoop && curToken != nil {
|
||||
tok = curToken
|
||||
firstLoop = false
|
||||
} else {
|
||||
tok, err = getNextToken(xmlParser)
|
||||
if err != nil || tok == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch tt := tok.(type) {
|
||||
case xml.SyntaxError:
|
||||
err = errors.New(tt.Error())
|
||||
return
|
||||
case xml.CharData:
|
||||
value := strings.TrimSpace(string([]byte(tt)))
|
||||
if node != nil {
|
||||
node.value += value
|
||||
}
|
||||
case xml.StartElement:
|
||||
if node == nil {
|
||||
node = newNode()
|
||||
err := node.unmarshal(tt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
childNode, childErr := unmarshalNode(xmlParser, tok)
|
||||
if childErr != nil {
|
||||
return nil, childErr
|
||||
}
|
||||
|
||||
if childNode != nil {
|
||||
node.add(childNode)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
case xml.EndElement:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNextToken(xmlParser *xml.Decoder) (tok xml.Token, err error) {
|
||||
if tok, err = xmlParser.Token(); err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
staticFuncCallDepth = 3 // See 'commonLogger.log' method comments
|
||||
loggerFuncCallDepth = 3
|
||||
)
|
||||
|
||||
// Current is the logger used in all package level convenience funcs like 'Trace', 'Debug', 'Flush', etc.
|
||||
var Current LoggerInterface
|
||||
|
||||
// Default logger that is created from an empty config: "<seelog/>". It is not closed by a ReplaceLogger call.
|
||||
var Default LoggerInterface
|
||||
|
||||
// Disabled logger that doesn't produce any output in any circumstances. It is neither closed nor flushed by a ReplaceLogger call.
|
||||
var Disabled LoggerInterface
|
||||
|
||||
var pkgOperationsMutex *sync.Mutex
|
||||
|
||||
func init() {
|
||||
pkgOperationsMutex = new(sync.Mutex)
|
||||
var err error
|
||||
|
||||
if Default == nil {
|
||||
Default, err = LoggerFromConfigAsBytes([]byte("<seelog />"))
|
||||
}
|
||||
|
||||
if Disabled == nil {
|
||||
Disabled, err = LoggerFromConfigAsBytes([]byte("<seelog levels=\"off\"/>"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Seelog couldn't start. Error: %s", err.Error()))
|
||||
}
|
||||
|
||||
Current = Default
|
||||
}
|
||||
|
||||
func createLoggerFromFullConfig(config *configForParsing) (LoggerInterface, error) {
|
||||
if config.LogType == syncloggerTypeFromString {
|
||||
return NewSyncLogger(&config.logConfig), nil
|
||||
} else if config.LogType == asyncLooploggerTypeFromString {
|
||||
return NewAsyncLoopLogger(&config.logConfig), nil
|
||||
} else if config.LogType == asyncTimerloggerTypeFromString {
|
||||
logData := config.LoggerData
|
||||
if logData == nil {
|
||||
return nil, errors.New("async timer data not set")
|
||||
}
|
||||
|
||||
asyncInt, ok := logData.(asyncTimerLoggerData)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid async timer data")
|
||||
}
|
||||
|
||||
logger, err := NewAsyncTimerLogger(&config.logConfig, time.Duration(asyncInt.AsyncInterval))
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logger, nil
|
||||
} else if config.LogType == adaptiveLoggerTypeFromString {
|
||||
logData := config.LoggerData
|
||||
if logData == nil {
|
||||
return nil, errors.New("adaptive logger parameters not set")
|
||||
}
|
||||
|
||||
adaptData, ok := logData.(adaptiveLoggerData)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid adaptive logger parameters")
|
||||
}
|
||||
|
||||
logger, err := NewAsyncAdaptiveLogger(
|
||||
&config.logConfig,
|
||||
time.Duration(adaptData.MinInterval),
|
||||
time.Duration(adaptData.MaxInterval),
|
||||
adaptData.CriticalMsgCount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
return nil, errors.New("invalid config log type/data")
|
||||
}
|
||||
|
||||
// UseLogger sets the 'Current' package level logger variable to the specified value.
|
||||
// This variable is used in all Trace/Debug/... package level convenience funcs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// after calling
|
||||
// seelog.UseLogger(somelogger)
|
||||
// the following:
|
||||
// seelog.Debug("abc")
|
||||
// will be equal to
|
||||
// somelogger.Debug("abc")
|
||||
//
|
||||
// IMPORTANT: UseLogger do NOT close the previous logger (only flushes it). So if
|
||||
// you constantly use it to replace loggers and don't close them in other code, you'll
|
||||
// end up having memory leaks.
|
||||
//
|
||||
// To safely replace loggers, use ReplaceLogger.
|
||||
func UseLogger(logger LoggerInterface) error {
|
||||
if logger == nil {
|
||||
return errors.New("logger can not be nil")
|
||||
}
|
||||
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
|
||||
oldLogger := Current
|
||||
Current = logger
|
||||
|
||||
if oldLogger != nil {
|
||||
oldLogger.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplaceLogger acts as UseLogger but the logger that was previously
|
||||
// used is disposed (except Default and Disabled loggers).
|
||||
//
|
||||
// Example:
|
||||
// import log "github.com/cihub/seelog"
|
||||
//
|
||||
// func main() {
|
||||
// logger, err := log.LoggerFromConfigAsFile("seelog.xml")
|
||||
//
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
//
|
||||
// log.ReplaceLogger(logger)
|
||||
// defer log.Flush()
|
||||
//
|
||||
// log.Trace("test")
|
||||
// log.Debugf("var = %s", "abc")
|
||||
// }
|
||||
func ReplaceLogger(logger LoggerInterface) error {
|
||||
if logger == nil {
|
||||
return errors.New("logger can not be nil")
|
||||
}
|
||||
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
reportInternalError(fmt.Errorf("recovered from panic during ReplaceLogger: %s", err))
|
||||
}
|
||||
}()
|
||||
|
||||
if Current == Default {
|
||||
Current.Flush()
|
||||
} else if Current != nil && !Current.Closed() && Current != Disabled {
|
||||
Current.Flush()
|
||||
Current.Close()
|
||||
}
|
||||
|
||||
Current = logger
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tracef formats message according to format specifier
|
||||
// and writes to default logger with log level = Trace.
|
||||
func Tracef(format string, params ...interface{}) {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
Current.traceWithCallDepth(staticFuncCallDepth, newLogFormattedMessage(format, params))
|
||||
}
|
||||
|
||||
// Debugf formats message according to format specifier
|
||||
// and writes to default logger with log level = Debug.
|
||||
func Debugf(format string, params ...interface{}) {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
Current.debugWithCallDepth(staticFuncCallDepth, newLogFormattedMessage(format, params))
|
||||
}
|
||||
|
||||
// Infof formats message according to format specifier
|
||||
// and writes to default logger with log level = Info.
|
||||
func Infof(format string, params ...interface{}) {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
Current.infoWithCallDepth(staticFuncCallDepth, newLogFormattedMessage(format, params))
|
||||
}
|
||||
|
||||
// Warnf formats message according to format specifier and writes to default logger with log level = Warn
|
||||
func Warnf(format string, params ...interface{}) error {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
message := newLogFormattedMessage(format, params)
|
||||
Current.warnWithCallDepth(staticFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
// Errorf formats message according to format specifier and writes to default logger with log level = Error
|
||||
func Errorf(format string, params ...interface{}) error {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
message := newLogFormattedMessage(format, params)
|
||||
Current.errorWithCallDepth(staticFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
// Criticalf formats message according to format specifier and writes to default logger with log level = Critical
|
||||
func Criticalf(format string, params ...interface{}) error {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
message := newLogFormattedMessage(format, params)
|
||||
Current.criticalWithCallDepth(staticFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
// Trace formats message using the default formats for its operands and writes to default logger with log level = Trace
|
||||
func Trace(v ...interface{}) {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
Current.traceWithCallDepth(staticFuncCallDepth, newLogMessage(v))
|
||||
}
|
||||
|
||||
// Debug formats message using the default formats for its operands and writes to default logger with log level = Debug
|
||||
func Debug(v ...interface{}) {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
Current.debugWithCallDepth(staticFuncCallDepth, newLogMessage(v))
|
||||
}
|
||||
|
||||
// Info formats message using the default formats for its operands and writes to default logger with log level = Info
|
||||
func Info(v ...interface{}) {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
Current.infoWithCallDepth(staticFuncCallDepth, newLogMessage(v))
|
||||
}
|
||||
|
||||
// Warn formats message using the default formats for its operands and writes to default logger with log level = Warn
|
||||
func Warn(v ...interface{}) error {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
message := newLogMessage(v)
|
||||
Current.warnWithCallDepth(staticFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
// Error formats message using the default formats for its operands and writes to default logger with log level = Error
|
||||
func Error(v ...interface{}) error {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
message := newLogMessage(v)
|
||||
Current.errorWithCallDepth(staticFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
// Critical formats message using the default formats for its operands and writes to default logger with log level = Critical
|
||||
func Critical(v ...interface{}) error {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
message := newLogMessage(v)
|
||||
Current.criticalWithCallDepth(staticFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
// Flush immediately processes all currently queued messages and all currently buffered messages.
|
||||
// It is a blocking call which returns only after the queue is empty and all the buffers are empty.
|
||||
//
|
||||
// If Flush is called for a synchronous logger (type='sync'), it only flushes buffers (e.g. '<buffered>' receivers)
|
||||
// , because there is no queue.
|
||||
//
|
||||
// Call this method when your app is going to shut down not to lose any log messages.
|
||||
func Flush() {
|
||||
pkgOperationsMutex.Lock()
|
||||
defer pkgOperationsMutex.Unlock()
|
||||
Current.Flush()
|
||||
}
|
||||
|
|
@ -1,370 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func reportInternalError(err error) {
|
||||
fmt.Fprintf(os.Stderr, "seelog internal error: %s\n", err)
|
||||
}
|
||||
|
||||
// LoggerInterface represents structs capable of logging Seelog messages
|
||||
type LoggerInterface interface {
|
||||
|
||||
// Tracef formats message according to format specifier
|
||||
// and writes to log with level = Trace.
|
||||
Tracef(format string, params ...interface{})
|
||||
|
||||
// Debugf formats message according to format specifier
|
||||
// and writes to log with level = Debug.
|
||||
Debugf(format string, params ...interface{})
|
||||
|
||||
// Infof formats message according to format specifier
|
||||
// and writes to log with level = Info.
|
||||
Infof(format string, params ...interface{})
|
||||
|
||||
// Warnf formats message according to format specifier
|
||||
// and writes to log with level = Warn.
|
||||
Warnf(format string, params ...interface{}) error
|
||||
|
||||
// Errorf formats message according to format specifier
|
||||
// and writes to log with level = Error.
|
||||
Errorf(format string, params ...interface{}) error
|
||||
|
||||
// Criticalf formats message according to format specifier
|
||||
// and writes to log with level = Critical.
|
||||
Criticalf(format string, params ...interface{}) error
|
||||
|
||||
// Trace formats message using the default formats for its operands
|
||||
// and writes to log with level = Trace
|
||||
Trace(v ...interface{})
|
||||
|
||||
// Debug formats message using the default formats for its operands
|
||||
// and writes to log with level = Debug
|
||||
Debug(v ...interface{})
|
||||
|
||||
// Info formats message using the default formats for its operands
|
||||
// and writes to log with level = Info
|
||||
Info(v ...interface{})
|
||||
|
||||
// Warn formats message using the default formats for its operands
|
||||
// and writes to log with level = Warn
|
||||
Warn(v ...interface{}) error
|
||||
|
||||
// Error formats message using the default formats for its operands
|
||||
// and writes to log with level = Error
|
||||
Error(v ...interface{}) error
|
||||
|
||||
// Critical formats message using the default formats for its operands
|
||||
// and writes to log with level = Critical
|
||||
Critical(v ...interface{}) error
|
||||
|
||||
traceWithCallDepth(callDepth int, message fmt.Stringer)
|
||||
debugWithCallDepth(callDepth int, message fmt.Stringer)
|
||||
infoWithCallDepth(callDepth int, message fmt.Stringer)
|
||||
warnWithCallDepth(callDepth int, message fmt.Stringer)
|
||||
errorWithCallDepth(callDepth int, message fmt.Stringer)
|
||||
criticalWithCallDepth(callDepth int, message fmt.Stringer)
|
||||
|
||||
// Close flushes all the messages in the logger and closes it. It cannot be used after this operation.
|
||||
Close()
|
||||
|
||||
// Flush flushes all the messages in the logger.
|
||||
Flush()
|
||||
|
||||
// Closed returns true if the logger was previously closed.
|
||||
Closed() bool
|
||||
|
||||
// SetAdditionalStackDepth sets the additional number of frames to skip by runtime.Caller
|
||||
// when getting function information needed to print seelog format identifiers such as %Func or %File.
|
||||
//
|
||||
// This func may be used when you wrap seelog funcs and want to print caller info of you own
|
||||
// wrappers instead of seelog func callers. In this case you should set depth = 1. If you then
|
||||
// wrap your wrapper, you should set depth = 2, etc.
|
||||
//
|
||||
// NOTE: Incorrect depth value may lead to errors in runtime.Caller evaluation or incorrect
|
||||
// function/file names in log files. Do not use it if you are not going to wrap seelog funcs.
|
||||
// You may reset the value to default using a SetAdditionalStackDepth(0) call.
|
||||
SetAdditionalStackDepth(depth int) error
|
||||
|
||||
// Sets logger context that can be used in formatter funcs and custom receivers
|
||||
SetContext(context interface{})
|
||||
}
|
||||
|
||||
// innerLoggerInterface is an internal logging interface
|
||||
type innerLoggerInterface interface {
|
||||
innerLog(level LogLevel, context LogContextInterface, message fmt.Stringer)
|
||||
Flush()
|
||||
}
|
||||
|
||||
// [file path][func name][level] -> [allowed]
|
||||
type allowedContextCache map[string]map[string]map[LogLevel]bool
|
||||
|
||||
// commonLogger contains all common data needed for logging and contains methods used to log messages.
|
||||
type commonLogger struct {
|
||||
config *logConfig // Config used for logging
|
||||
contextCache allowedContextCache // Caches whether log is enabled for specific "full path-func name-level" sets
|
||||
closed bool // 'true' when all writers are closed, all data is flushed, logger is unusable. Must be accessed while holding closedM
|
||||
closedM sync.RWMutex
|
||||
m sync.Mutex // Mutex for main operations
|
||||
unusedLevels []bool
|
||||
innerLogger innerLoggerInterface
|
||||
addStackDepth int // Additional stack depth needed for correct seelog caller context detection
|
||||
customContext interface{}
|
||||
}
|
||||
|
||||
func newCommonLogger(config *logConfig, internalLogger innerLoggerInterface) *commonLogger {
|
||||
cLogger := new(commonLogger)
|
||||
|
||||
cLogger.config = config
|
||||
cLogger.contextCache = make(allowedContextCache)
|
||||
cLogger.unusedLevels = make([]bool, Off)
|
||||
cLogger.fillUnusedLevels()
|
||||
cLogger.innerLogger = internalLogger
|
||||
|
||||
return cLogger
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) SetAdditionalStackDepth(depth int) error {
|
||||
if depth < 0 {
|
||||
return fmt.Errorf("negative depth: %d", depth)
|
||||
}
|
||||
cLogger.m.Lock()
|
||||
cLogger.addStackDepth = depth
|
||||
cLogger.m.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Tracef(format string, params ...interface{}) {
|
||||
cLogger.traceWithCallDepth(loggerFuncCallDepth, newLogFormattedMessage(format, params))
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Debugf(format string, params ...interface{}) {
|
||||
cLogger.debugWithCallDepth(loggerFuncCallDepth, newLogFormattedMessage(format, params))
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Infof(format string, params ...interface{}) {
|
||||
cLogger.infoWithCallDepth(loggerFuncCallDepth, newLogFormattedMessage(format, params))
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Warnf(format string, params ...interface{}) error {
|
||||
message := newLogFormattedMessage(format, params)
|
||||
cLogger.warnWithCallDepth(loggerFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Errorf(format string, params ...interface{}) error {
|
||||
message := newLogFormattedMessage(format, params)
|
||||
cLogger.errorWithCallDepth(loggerFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Criticalf(format string, params ...interface{}) error {
|
||||
message := newLogFormattedMessage(format, params)
|
||||
cLogger.criticalWithCallDepth(loggerFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Trace(v ...interface{}) {
|
||||
cLogger.traceWithCallDepth(loggerFuncCallDepth, newLogMessage(v))
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Debug(v ...interface{}) {
|
||||
cLogger.debugWithCallDepth(loggerFuncCallDepth, newLogMessage(v))
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Info(v ...interface{}) {
|
||||
cLogger.infoWithCallDepth(loggerFuncCallDepth, newLogMessage(v))
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Warn(v ...interface{}) error {
|
||||
message := newLogMessage(v)
|
||||
cLogger.warnWithCallDepth(loggerFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Error(v ...interface{}) error {
|
||||
message := newLogMessage(v)
|
||||
cLogger.errorWithCallDepth(loggerFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Critical(v ...interface{}) error {
|
||||
message := newLogMessage(v)
|
||||
cLogger.criticalWithCallDepth(loggerFuncCallDepth, message)
|
||||
return errors.New(message.String())
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) SetContext(c interface{}) {
|
||||
cLogger.customContext = c
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) traceWithCallDepth(callDepth int, message fmt.Stringer) {
|
||||
cLogger.log(TraceLvl, message, callDepth)
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) debugWithCallDepth(callDepth int, message fmt.Stringer) {
|
||||
cLogger.log(DebugLvl, message, callDepth)
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) infoWithCallDepth(callDepth int, message fmt.Stringer) {
|
||||
cLogger.log(InfoLvl, message, callDepth)
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) warnWithCallDepth(callDepth int, message fmt.Stringer) {
|
||||
cLogger.log(WarnLvl, message, callDepth)
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) errorWithCallDepth(callDepth int, message fmt.Stringer) {
|
||||
cLogger.log(ErrorLvl, message, callDepth)
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) criticalWithCallDepth(callDepth int, message fmt.Stringer) {
|
||||
cLogger.log(CriticalLvl, message, callDepth)
|
||||
cLogger.innerLogger.Flush()
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) Closed() bool {
|
||||
cLogger.closedM.RLock()
|
||||
defer cLogger.closedM.RUnlock()
|
||||
return cLogger.closed
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) fillUnusedLevels() {
|
||||
for i := 0; i < len(cLogger.unusedLevels); i++ {
|
||||
cLogger.unusedLevels[i] = true
|
||||
}
|
||||
|
||||
cLogger.fillUnusedLevelsByContraint(cLogger.config.Constraints)
|
||||
|
||||
for _, exception := range cLogger.config.Exceptions {
|
||||
cLogger.fillUnusedLevelsByContraint(exception)
|
||||
}
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) fillUnusedLevelsByContraint(constraint logLevelConstraints) {
|
||||
for i := 0; i < len(cLogger.unusedLevels); i++ {
|
||||
if constraint.IsAllowed(LogLevel(i)) {
|
||||
cLogger.unusedLevels[i] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stackCallDepth is used to indicate the call depth of 'log' func.
|
||||
// This depth level is used in the runtime.Caller(...) call. See
|
||||
// common_context.go -> specifyContext, extractCallerInfo for details.
|
||||
func (cLogger *commonLogger) log(level LogLevel, message fmt.Stringer, stackCallDepth int) {
|
||||
if cLogger.unusedLevels[level] {
|
||||
return
|
||||
}
|
||||
cLogger.m.Lock()
|
||||
defer cLogger.m.Unlock()
|
||||
|
||||
if cLogger.Closed() {
|
||||
return
|
||||
}
|
||||
context, _ := specifyContext(stackCallDepth+cLogger.addStackDepth, cLogger.customContext)
|
||||
// Context errors are not reported because there are situations
|
||||
// in which context errors are normal Seelog usage cases. For
|
||||
// example in executables with stripped symbols.
|
||||
// Error contexts are returned instead. See common_context.go.
|
||||
/*if err != nil {
|
||||
reportInternalError(err)
|
||||
return
|
||||
}*/
|
||||
cLogger.innerLogger.innerLog(level, context, message)
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) processLogMsg(level LogLevel, message fmt.Stringer, context LogContextInterface) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
reportInternalError(fmt.Errorf("recovered from panic during message processing: %s", err))
|
||||
}
|
||||
}()
|
||||
if cLogger.config.IsAllowed(level, context) {
|
||||
cLogger.config.RootDispatcher.Dispatch(message.String(), level, context, reportInternalError)
|
||||
}
|
||||
}
|
||||
|
||||
func (cLogger *commonLogger) isAllowed(level LogLevel, context LogContextInterface) bool {
|
||||
funcMap, ok := cLogger.contextCache[context.FullPath()]
|
||||
if !ok {
|
||||
funcMap = make(map[string]map[LogLevel]bool, 0)
|
||||
cLogger.contextCache[context.FullPath()] = funcMap
|
||||
}
|
||||
|
||||
levelMap, ok := funcMap[context.Func()]
|
||||
if !ok {
|
||||
levelMap = make(map[LogLevel]bool, 0)
|
||||
funcMap[context.Func()] = levelMap
|
||||
}
|
||||
|
||||
isAllowValue, ok := levelMap[level]
|
||||
if !ok {
|
||||
isAllowValue = cLogger.config.IsAllowed(level, context)
|
||||
levelMap[level] = isAllowValue
|
||||
}
|
||||
|
||||
return isAllowValue
|
||||
}
|
||||
|
||||
type logMessage struct {
|
||||
params []interface{}
|
||||
}
|
||||
|
||||
type logFormattedMessage struct {
|
||||
format string
|
||||
params []interface{}
|
||||
}
|
||||
|
||||
func newLogMessage(params []interface{}) fmt.Stringer {
|
||||
message := new(logMessage)
|
||||
|
||||
message.params = params
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func newLogFormattedMessage(format string, params []interface{}) *logFormattedMessage {
|
||||
message := new(logFormattedMessage)
|
||||
|
||||
message.params = params
|
||||
message.format = format
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func (message *logMessage) String() string {
|
||||
return fmt.Sprint(message.params...)
|
||||
}
|
||||
|
||||
func (message *logFormattedMessage) String() string {
|
||||
return fmt.Sprintf(message.format, message.params...)
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// bufferedWriter stores data in memory and flushes it every flushPeriod or when buffer is full
|
||||
type bufferedWriter struct {
|
||||
flushPeriod time.Duration // data flushes interval (in microseconds)
|
||||
bufferMutex *sync.Mutex // mutex for buffer operations syncronization
|
||||
innerWriter io.Writer // inner writer
|
||||
buffer *bufio.Writer // buffered wrapper for inner writer
|
||||
bufferSize int // max size of data chunk in bytes
|
||||
}
|
||||
|
||||
// NewBufferedWriter creates a new buffered writer struct.
|
||||
// bufferSize -- size of memory buffer in bytes
|
||||
// flushPeriod -- period in which data flushes from memory buffer in milliseconds. 0 - turn off this functionality
|
||||
func NewBufferedWriter(innerWriter io.Writer, bufferSize int, flushPeriod time.Duration) (*bufferedWriter, error) {
|
||||
|
||||
if innerWriter == nil {
|
||||
return nil, errors.New("argument is nil: innerWriter")
|
||||
}
|
||||
if flushPeriod < 0 {
|
||||
return nil, fmt.Errorf("flushPeriod can not be less than 0. Got: %d", flushPeriod)
|
||||
}
|
||||
|
||||
if bufferSize <= 0 {
|
||||
return nil, fmt.Errorf("bufferSize can not be less or equal to 0. Got: %d", bufferSize)
|
||||
}
|
||||
|
||||
buffer := bufio.NewWriterSize(innerWriter, bufferSize)
|
||||
|
||||
/*if err != nil {
|
||||
return nil, err
|
||||
}*/
|
||||
|
||||
newWriter := new(bufferedWriter)
|
||||
|
||||
newWriter.innerWriter = innerWriter
|
||||
newWriter.buffer = buffer
|
||||
newWriter.bufferSize = bufferSize
|
||||
newWriter.flushPeriod = flushPeriod * 1e6
|
||||
newWriter.bufferMutex = new(sync.Mutex)
|
||||
|
||||
if flushPeriod != 0 {
|
||||
go newWriter.flushPeriodically()
|
||||
}
|
||||
|
||||
return newWriter, nil
|
||||
}
|
||||
|
||||
func (bufWriter *bufferedWriter) writeBigChunk(bytes []byte) (n int, err error) {
|
||||
bufferedLen := bufWriter.buffer.Buffered()
|
||||
|
||||
n, err = bufWriter.flushInner()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
written, writeErr := bufWriter.innerWriter.Write(bytes)
|
||||
return bufferedLen + written, writeErr
|
||||
}
|
||||
|
||||
// Sends data to buffer manager. Waits until all buffers are full.
|
||||
func (bufWriter *bufferedWriter) Write(bytes []byte) (n int, err error) {
|
||||
|
||||
bufWriter.bufferMutex.Lock()
|
||||
defer bufWriter.bufferMutex.Unlock()
|
||||
|
||||
bytesLen := len(bytes)
|
||||
|
||||
if bytesLen > bufWriter.bufferSize {
|
||||
return bufWriter.writeBigChunk(bytes)
|
||||
}
|
||||
|
||||
if bytesLen > bufWriter.buffer.Available() {
|
||||
n, err = bufWriter.flushInner()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bufWriter.buffer.Write(bytes)
|
||||
|
||||
return len(bytes), nil
|
||||
}
|
||||
|
||||
func (bufWriter *bufferedWriter) Close() error {
|
||||
closer, ok := bufWriter.innerWriter.(io.Closer)
|
||||
if ok {
|
||||
return closer.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bufWriter *bufferedWriter) Flush() {
|
||||
|
||||
bufWriter.bufferMutex.Lock()
|
||||
defer bufWriter.bufferMutex.Unlock()
|
||||
|
||||
bufWriter.flushInner()
|
||||
}
|
||||
|
||||
func (bufWriter *bufferedWriter) flushInner() (n int, err error) {
|
||||
bufferedLen := bufWriter.buffer.Buffered()
|
||||
flushErr := bufWriter.buffer.Flush()
|
||||
|
||||
return bufWriter.buffer.Buffered() - bufferedLen, flushErr
|
||||
}
|
||||
|
||||
func (bufWriter *bufferedWriter) flushBuffer() {
|
||||
bufWriter.bufferMutex.Lock()
|
||||
defer bufWriter.bufferMutex.Unlock()
|
||||
|
||||
bufWriter.buffer.Flush()
|
||||
}
|
||||
|
||||
func (bufWriter *bufferedWriter) flushPeriodically() {
|
||||
if bufWriter.flushPeriod > 0 {
|
||||
ticker := time.NewTicker(bufWriter.flushPeriod)
|
||||
for {
|
||||
<-ticker.C
|
||||
bufWriter.flushBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bufWriter *bufferedWriter) String() string {
|
||||
return fmt.Sprintf("bufferedWriter size: %d, flushPeriod: %d", bufWriter.bufferSize, bufWriter.flushPeriod)
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// connWriter is used to write to a stream-oriented network connection.
|
||||
type connWriter struct {
|
||||
innerWriter io.WriteCloser
|
||||
reconnectOnMsg bool
|
||||
reconnect bool
|
||||
net string
|
||||
addr string
|
||||
useTLS bool
|
||||
configTLS *tls.Config
|
||||
}
|
||||
|
||||
// Creates writer to the address addr on the network netName.
|
||||
// Connection will be opened on each write if reconnectOnMsg = true
|
||||
func NewConnWriter(netName string, addr string, reconnectOnMsg bool) *connWriter {
|
||||
newWriter := new(connWriter)
|
||||
|
||||
newWriter.net = netName
|
||||
newWriter.addr = addr
|
||||
newWriter.reconnectOnMsg = reconnectOnMsg
|
||||
|
||||
return newWriter
|
||||
}
|
||||
|
||||
// Creates a writer that uses SSL/TLS
|
||||
func newTLSWriter(netName string, addr string, reconnectOnMsg bool, config *tls.Config) *connWriter {
|
||||
newWriter := new(connWriter)
|
||||
|
||||
newWriter.net = netName
|
||||
newWriter.addr = addr
|
||||
newWriter.reconnectOnMsg = reconnectOnMsg
|
||||
newWriter.useTLS = true
|
||||
newWriter.configTLS = config
|
||||
|
||||
return newWriter
|
||||
}
|
||||
|
||||
func (connWriter *connWriter) Close() error {
|
||||
if connWriter.innerWriter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return connWriter.innerWriter.Close()
|
||||
}
|
||||
|
||||
func (connWriter *connWriter) Write(bytes []byte) (n int, err error) {
|
||||
if connWriter.neededConnectOnMsg() {
|
||||
err = connWriter.connect()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if connWriter.reconnectOnMsg {
|
||||
defer connWriter.innerWriter.Close()
|
||||
}
|
||||
|
||||
n, err = connWriter.innerWriter.Write(bytes)
|
||||
if err != nil {
|
||||
connWriter.reconnect = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (connWriter *connWriter) String() string {
|
||||
return fmt.Sprintf("Conn writer: [%s, %s, %v]", connWriter.net, connWriter.addr, connWriter.reconnectOnMsg)
|
||||
}
|
||||
|
||||
func (connWriter *connWriter) connect() error {
|
||||
if connWriter.innerWriter != nil {
|
||||
connWriter.innerWriter.Close()
|
||||
connWriter.innerWriter = nil
|
||||
}
|
||||
|
||||
if connWriter.useTLS {
|
||||
conn, err := tls.Dial(connWriter.net, connWriter.addr, connWriter.configTLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connWriter.innerWriter = conn
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
conn, err := net.Dial(connWriter.net, connWriter.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tcpConn, ok := conn.(*net.TCPConn)
|
||||
if ok {
|
||||
tcpConn.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
connWriter.innerWriter = conn
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (connWriter *connWriter) neededConnectOnMsg() bool {
|
||||
if connWriter.reconnect {
|
||||
connWriter.reconnect = false
|
||||
return true
|
||||
}
|
||||
|
||||
if connWriter.innerWriter == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return connWriter.reconnectOnMsg
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import "fmt"
|
||||
|
||||
// consoleWriter is used to write to console
|
||||
type consoleWriter struct {
|
||||
}
|
||||
|
||||
// Creates a new console writer. Returns error, if the console writer couldn't be created.
|
||||
func NewConsoleWriter() (writer *consoleWriter, err error) {
|
||||
newWriter := new(consoleWriter)
|
||||
|
||||
return newWriter, nil
|
||||
}
|
||||
|
||||
// Create folder and file on WriteLog/Write first call
|
||||
func (console *consoleWriter) Write(bytes []byte) (int, error) {
|
||||
return fmt.Print(string(bytes))
|
||||
}
|
||||
|
||||
func (console *consoleWriter) String() string {
|
||||
return "Console writer"
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// fileWriter is used to write to a file.
|
||||
type fileWriter struct {
|
||||
innerWriter io.WriteCloser
|
||||
fileName string
|
||||
}
|
||||
|
||||
// Creates a new file and a corresponding writer. Returns error, if the file couldn't be created.
|
||||
func NewFileWriter(fileName string) (writer *fileWriter, err error) {
|
||||
newWriter := new(fileWriter)
|
||||
newWriter.fileName = fileName
|
||||
|
||||
return newWriter, nil
|
||||
}
|
||||
|
||||
func (fw *fileWriter) Close() error {
|
||||
if fw.innerWriter != nil {
|
||||
err := fw.innerWriter.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fw.innerWriter = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create folder and file on WriteLog/Write first call
|
||||
func (fw *fileWriter) Write(bytes []byte) (n int, err error) {
|
||||
if fw.innerWriter == nil {
|
||||
if err := fw.createFile(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return fw.innerWriter.Write(bytes)
|
||||
}
|
||||
|
||||
func (fw *fileWriter) createFile() error {
|
||||
folder, _ := filepath.Split(fw.fileName)
|
||||
var err error
|
||||
|
||||
if 0 != len(folder) {
|
||||
err = os.MkdirAll(folder, defaultDirectoryPermissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If exists
|
||||
fw.innerWriter, err = os.OpenFile(fw.fileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, defaultFilePermissions)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fw *fileWriter) String() string {
|
||||
return fmt.Sprintf("File writer: %s", fw.fileName)
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type formattedWriter struct {
|
||||
writer io.Writer
|
||||
formatter *formatter
|
||||
}
|
||||
|
||||
func NewFormattedWriter(writer io.Writer, formatter *formatter) (*formattedWriter, error) {
|
||||
if formatter == nil {
|
||||
return nil, errors.New("formatter can not be nil")
|
||||
}
|
||||
|
||||
return &formattedWriter{writer, formatter}, nil
|
||||
}
|
||||
|
||||
func (formattedWriter *formattedWriter) Write(message string, level LogLevel, context LogContextInterface) error {
|
||||
str := formattedWriter.formatter.Format(message, level, context)
|
||||
_, err := formattedWriter.writer.Write([]byte(str))
|
||||
return err
|
||||
}
|
||||
|
||||
func (formattedWriter *formattedWriter) String() string {
|
||||
return fmt.Sprintf("writer: %s, format: %s", formattedWriter.writer, formattedWriter.formatter)
|
||||
}
|
||||
|
||||
func (formattedWriter *formattedWriter) Writer() io.Writer {
|
||||
return formattedWriter.writer
|
||||
}
|
||||
|
||||
func (formattedWriter *formattedWriter) Format() *formatter {
|
||||
return formattedWriter.formatter
|
||||
}
|
||||
|
|
@ -1,625 +0,0 @@
|
|||
// Copyright (c) 2013 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Common constants
|
||||
const (
|
||||
rollingLogHistoryDelimiter = "."
|
||||
)
|
||||
|
||||
// Types of the rolling writer: roll by date, by time, etc.
|
||||
type rollingType uint8
|
||||
|
||||
const (
|
||||
rollingTypeSize = iota
|
||||
rollingTypeTime
|
||||
)
|
||||
|
||||
// Types of the rolled file naming mode: prefix, postfix, etc.
|
||||
type rollingNameMode uint8
|
||||
|
||||
const (
|
||||
rollingNameModePostfix = iota
|
||||
rollingNameModePrefix
|
||||
)
|
||||
|
||||
var rollingNameModesStringRepresentation = map[rollingNameMode]string{
|
||||
rollingNameModePostfix: "postfix",
|
||||
rollingNameModePrefix: "prefix",
|
||||
}
|
||||
|
||||
func rollingNameModeFromString(rollingNameStr string) (rollingNameMode, bool) {
|
||||
for tp, tpStr := range rollingNameModesStringRepresentation {
|
||||
if tpStr == rollingNameStr {
|
||||
return tp, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
type rollingIntervalType uint8
|
||||
|
||||
const (
|
||||
rollingIntervalAny = iota
|
||||
rollingIntervalDaily
|
||||
)
|
||||
|
||||
var rollingInvervalTypesStringRepresentation = map[rollingIntervalType]string{
|
||||
rollingIntervalDaily: "daily",
|
||||
}
|
||||
|
||||
func rollingIntervalTypeFromString(rollingTypeStr string) (rollingIntervalType, bool) {
|
||||
for tp, tpStr := range rollingInvervalTypesStringRepresentation {
|
||||
if tpStr == rollingTypeStr {
|
||||
return tp, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var rollingTypesStringRepresentation = map[rollingType]string{
|
||||
rollingTypeSize: "size",
|
||||
rollingTypeTime: "date",
|
||||
}
|
||||
|
||||
func rollingTypeFromString(rollingTypeStr string) (rollingType, bool) {
|
||||
for tp, tpStr := range rollingTypesStringRepresentation {
|
||||
if tpStr == rollingTypeStr {
|
||||
return tp, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Old logs archivation type.
|
||||
type rollingArchiveType uint8
|
||||
|
||||
const (
|
||||
rollingArchiveNone = iota
|
||||
rollingArchiveZip
|
||||
)
|
||||
|
||||
var rollingArchiveTypesStringRepresentation = map[rollingArchiveType]string{
|
||||
rollingArchiveNone: "none",
|
||||
rollingArchiveZip: "zip",
|
||||
}
|
||||
|
||||
func rollingArchiveTypeFromString(rollingArchiveTypeStr string) (rollingArchiveType, bool) {
|
||||
for tp, tpStr := range rollingArchiveTypesStringRepresentation {
|
||||
if tpStr == rollingArchiveTypeStr {
|
||||
return tp, true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Default names for different archivation types
|
||||
var rollingArchiveTypesDefaultNames = map[rollingArchiveType]string{
|
||||
rollingArchiveZip: "log.zip",
|
||||
}
|
||||
|
||||
// rollerVirtual is an interface that represents all virtual funcs that are
|
||||
// called in different rolling writer subtypes.
|
||||
type rollerVirtual interface {
|
||||
needsToRoll() (bool, error) // Returns true if needs to switch to another file.
|
||||
isFileRollNameValid(rname string) bool // Returns true if logger roll file name (postfix/prefix/etc.) is ok.
|
||||
sortFileRollNamesAsc(fs []string) ([]string, error) // Sorts logger roll file names in ascending order of their creation by logger.
|
||||
|
||||
// Creates a new froll history file using the contents of current file and special filename of the latest roll (prefix/ postfix).
|
||||
// If lastRollName is empty (""), then it means that there is no latest roll (current is the first one)
|
||||
getNewHistoryRollFileName(lastRollName string) string
|
||||
getCurrentModifiedFileName(originalFileName string, first bool) (string, error) // Returns filename modified according to specific logger rules
|
||||
}
|
||||
|
||||
// rollingFileWriter writes received messages to a file, until time interval passes
|
||||
// or file exceeds a specified limit. After that the current log file is renamed
|
||||
// and writer starts to log into a new file. You can set a limit for such renamed
|
||||
// files count, if you want, and then the rolling writer would delete older ones when
|
||||
// the files count exceed the specified limit.
|
||||
type rollingFileWriter struct {
|
||||
fileName string // current file name. May differ from original in date rolling loggers
|
||||
originalFileName string // original one
|
||||
currentDirPath string
|
||||
currentFile *os.File
|
||||
currentFileSize int64
|
||||
rollingType rollingType // Rolling mode (Files roll by size/date/...)
|
||||
archiveType rollingArchiveType
|
||||
archivePath string
|
||||
maxRolls int
|
||||
nameMode rollingNameMode
|
||||
self rollerVirtual // Used for virtual calls
|
||||
}
|
||||
|
||||
func newRollingFileWriter(fpath string, rtype rollingType, atype rollingArchiveType, apath string, maxr int, namemode rollingNameMode) (*rollingFileWriter, error) {
|
||||
rw := new(rollingFileWriter)
|
||||
rw.currentDirPath, rw.fileName = filepath.Split(fpath)
|
||||
if len(rw.currentDirPath) == 0 {
|
||||
rw.currentDirPath = "."
|
||||
}
|
||||
rw.originalFileName = rw.fileName
|
||||
|
||||
rw.rollingType = rtype
|
||||
rw.archiveType = atype
|
||||
rw.archivePath = apath
|
||||
rw.nameMode = namemode
|
||||
rw.maxRolls = maxr
|
||||
return rw, nil
|
||||
}
|
||||
|
||||
func (rw *rollingFileWriter) hasRollName(file string) bool {
|
||||
switch rw.nameMode {
|
||||
case rollingNameModePostfix:
|
||||
rname := rw.originalFileName + rollingLogHistoryDelimiter
|
||||
return strings.HasPrefix(file, rname)
|
||||
case rollingNameModePrefix:
|
||||
rname := rollingLogHistoryDelimiter + rw.originalFileName
|
||||
return strings.HasSuffix(file, rname)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rw *rollingFileWriter) createFullFileName(originalName, rollname string) string {
|
||||
switch rw.nameMode {
|
||||
case rollingNameModePostfix:
|
||||
return originalName + rollingLogHistoryDelimiter + rollname
|
||||
case rollingNameModePrefix:
|
||||
return rollname + rollingLogHistoryDelimiter + originalName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (rw *rollingFileWriter) getSortedLogHistory() ([]string, error) {
|
||||
files, err := getDirFilePaths(rw.currentDirPath, nil, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var validRollNames []string
|
||||
for _, file := range files {
|
||||
if file != rw.fileName && rw.hasRollName(file) {
|
||||
rname := rw.getFileRollName(file)
|
||||
if rw.self.isFileRollNameValid(rname) {
|
||||
validRollNames = append(validRollNames, rname)
|
||||
}
|
||||
}
|
||||
}
|
||||
sortedTails, err := rw.self.sortFileRollNamesAsc(validRollNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validSortedFiles := make([]string, len(sortedTails))
|
||||
for i, v := range sortedTails {
|
||||
validSortedFiles[i] = rw.createFullFileName(rw.originalFileName, v)
|
||||
}
|
||||
return validSortedFiles, nil
|
||||
}
|
||||
|
||||
func (rw *rollingFileWriter) createFileAndFolderIfNeeded(first bool) error {
|
||||
var err error
|
||||
|
||||
if len(rw.currentDirPath) != 0 {
|
||||
err = os.MkdirAll(rw.currentDirPath, defaultDirectoryPermissions)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rw.fileName, err = rw.self.getCurrentModifiedFileName(rw.originalFileName, first)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filePath := filepath.Join(rw.currentDirPath, rw.fileName)
|
||||
|
||||
// If exists
|
||||
stat, err := os.Lstat(filePath)
|
||||
if err == nil {
|
||||
rw.currentFile, err = os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, defaultFilePermissions)
|
||||
|
||||
stat, err = os.Lstat(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rw.currentFileSize = stat.Size()
|
||||
} else {
|
||||
rw.currentFile, err = os.Create(filePath)
|
||||
rw.currentFileSize = 0
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rw *rollingFileWriter) deleteOldRolls(history []string) error {
|
||||
if rw.maxRolls <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rollsToDelete := len(history) - rw.maxRolls
|
||||
if rollsToDelete <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch rw.archiveType {
|
||||
case rollingArchiveZip:
|
||||
var files map[string][]byte
|
||||
|
||||
// If archive exists
|
||||
_, err := os.Lstat(rw.archivePath)
|
||||
if nil == err {
|
||||
// Extract files and content from it
|
||||
files, err = unzip(rw.archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove the original file
|
||||
err = tryRemoveFile(rw.archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
files = make(map[string][]byte)
|
||||
}
|
||||
|
||||
// Add files to the existing files map, filled above
|
||||
for i := 0; i < rollsToDelete; i++ {
|
||||
rollPath := filepath.Join(rw.currentDirPath, history[i])
|
||||
bts, err := ioutil.ReadFile(rollPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files[rollPath] = bts
|
||||
}
|
||||
|
||||
// Put the final file set to zip file.
|
||||
if err = createZip(rw.archivePath, files); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var err error
|
||||
// In all cases (archive files or not) the files should be deleted.
|
||||
for i := 0; i < rollsToDelete; i++ {
|
||||
// Try best to delete files without breaking the loop.
|
||||
if err = tryRemoveFile(filepath.Join(rw.currentDirPath, history[i])); err != nil {
|
||||
reportInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rw *rollingFileWriter) getFileRollName(fileName string) string {
|
||||
switch rw.nameMode {
|
||||
case rollingNameModePostfix:
|
||||
return fileName[len(rw.originalFileName+rollingLogHistoryDelimiter):]
|
||||
case rollingNameModePrefix:
|
||||
return fileName[:len(fileName)-len(rw.originalFileName+rollingLogHistoryDelimiter)]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (rw *rollingFileWriter) Write(bytes []byte) (n int, err error) {
|
||||
if rw.currentFile == nil {
|
||||
err := rw.createFileAndFolderIfNeeded(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
// needs to roll if:
|
||||
// * file roller max file size exceeded OR
|
||||
// * time roller interval passed
|
||||
nr, err := rw.self.needsToRoll()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if nr {
|
||||
// First, close current file.
|
||||
err = rw.currentFile.Close()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Current history of all previous log files.
|
||||
// For file roller it may be like this:
|
||||
// * ...
|
||||
// * file.log.4
|
||||
// * file.log.5
|
||||
// * file.log.6
|
||||
//
|
||||
// For date roller it may look like this:
|
||||
// * ...
|
||||
// * file.log.11.Aug.13
|
||||
// * file.log.15.Aug.13
|
||||
// * file.log.16.Aug.13
|
||||
// Sorted log history does NOT include current file.
|
||||
history, err := rw.getSortedLogHistory()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Renames current file to create a new roll history entry
|
||||
// For file roller it may be like this:
|
||||
// * ...
|
||||
// * file.log.4
|
||||
// * file.log.5
|
||||
// * file.log.6
|
||||
// n file.log.7 <---- RENAMED (from file.log)
|
||||
// Time rollers that doesn't modify file names (e.g. 'date' roller) skip this logic.
|
||||
var newHistoryName string
|
||||
var newRollMarkerName string
|
||||
if len(history) > 0 {
|
||||
// Create new rname name using last history file name
|
||||
newRollMarkerName = rw.self.getNewHistoryRollFileName(rw.getFileRollName(history[len(history)-1]))
|
||||
} else {
|
||||
// Create first rname name
|
||||
newRollMarkerName = rw.self.getNewHistoryRollFileName("")
|
||||
}
|
||||
if len(newRollMarkerName) != 0 {
|
||||
newHistoryName = rw.createFullFileName(rw.fileName, newRollMarkerName)
|
||||
} else {
|
||||
newHistoryName = rw.fileName
|
||||
}
|
||||
if newHistoryName != rw.fileName {
|
||||
err = os.Rename(filepath.Join(rw.currentDirPath, rw.fileName), filepath.Join(rw.currentDirPath, newHistoryName))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
// Finally, add the newly added history file to the history archive
|
||||
// and, if after that the archive exceeds the allowed max limit, older rolls
|
||||
// must the removed/archived.
|
||||
history = append(history, newHistoryName)
|
||||
if len(history) > rw.maxRolls {
|
||||
err = rw.deleteOldRolls(history)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
err = rw.createFileAndFolderIfNeeded(false)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
rw.currentFileSize += int64(len(bytes))
|
||||
return rw.currentFile.Write(bytes)
|
||||
}
|
||||
|
||||
func (rw *rollingFileWriter) Close() error {
|
||||
if rw.currentFile != nil {
|
||||
e := rw.currentFile.Close()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
rw.currentFile = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================================
|
||||
// Different types of rolling writers
|
||||
// =============================================================================================
|
||||
|
||||
// --------------------------------------------------
|
||||
// Rolling writer by SIZE
|
||||
// --------------------------------------------------
|
||||
|
||||
// rollingFileWriterSize performs roll when file exceeds a specified limit.
|
||||
type rollingFileWriterSize struct {
|
||||
*rollingFileWriter
|
||||
maxFileSize int64
|
||||
}
|
||||
|
||||
func NewRollingFileWriterSize(fpath string, atype rollingArchiveType, apath string, maxSize int64, maxRolls int, namemode rollingNameMode) (*rollingFileWriterSize, error) {
|
||||
rw, err := newRollingFileWriter(fpath, rollingTypeSize, atype, apath, maxRolls, namemode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rws := &rollingFileWriterSize{rw, maxSize}
|
||||
rws.self = rws
|
||||
return rws, nil
|
||||
}
|
||||
|
||||
func (rws *rollingFileWriterSize) needsToRoll() (bool, error) {
|
||||
return rws.currentFileSize >= rws.maxFileSize, nil
|
||||
}
|
||||
|
||||
func (rws *rollingFileWriterSize) isFileRollNameValid(rname string) bool {
|
||||
if len(rname) == 0 {
|
||||
return false
|
||||
}
|
||||
_, err := strconv.Atoi(rname)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
type rollSizeFileTailsSlice []string
|
||||
|
||||
func (p rollSizeFileTailsSlice) Len() int { return len(p) }
|
||||
func (p rollSizeFileTailsSlice) Less(i, j int) bool {
|
||||
v1, _ := strconv.Atoi(p[i])
|
||||
v2, _ := strconv.Atoi(p[j])
|
||||
return v1 < v2
|
||||
}
|
||||
func (p rollSizeFileTailsSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
func (rws *rollingFileWriterSize) sortFileRollNamesAsc(fs []string) ([]string, error) {
|
||||
ss := rollSizeFileTailsSlice(fs)
|
||||
sort.Sort(ss)
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
func (rws *rollingFileWriterSize) getNewHistoryRollFileName(lastRollName string) string {
|
||||
v := 0
|
||||
if len(lastRollName) != 0 {
|
||||
v, _ = strconv.Atoi(lastRollName)
|
||||
}
|
||||
return fmt.Sprintf("%d", v+1)
|
||||
}
|
||||
|
||||
func (rws *rollingFileWriterSize) getCurrentModifiedFileName(originalFileName string, first bool) (string, error) {
|
||||
return originalFileName, nil
|
||||
}
|
||||
|
||||
func (rws *rollingFileWriterSize) String() string {
|
||||
return fmt.Sprintf("Rolling file writer (By SIZE): filename: %s, archive: %s, archivefile: %s, maxFileSize: %v, maxRolls: %v",
|
||||
rws.fileName,
|
||||
rollingArchiveTypesStringRepresentation[rws.archiveType],
|
||||
rws.archivePath,
|
||||
rws.maxFileSize,
|
||||
rws.maxRolls)
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Rolling writer by TIME
|
||||
// --------------------------------------------------
|
||||
|
||||
// rollingFileWriterTime performs roll when a specified time interval has passed.
|
||||
type rollingFileWriterTime struct {
|
||||
*rollingFileWriter
|
||||
timePattern string
|
||||
interval rollingIntervalType
|
||||
currentTimeFileName string
|
||||
}
|
||||
|
||||
func NewRollingFileWriterTime(fpath string, atype rollingArchiveType, apath string, maxr int,
|
||||
timePattern string, interval rollingIntervalType, namemode rollingNameMode) (*rollingFileWriterTime, error) {
|
||||
|
||||
rw, err := newRollingFileWriter(fpath, rollingTypeTime, atype, apath, maxr, namemode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rws := &rollingFileWriterTime{rw, timePattern, interval, ""}
|
||||
rws.self = rws
|
||||
return rws, nil
|
||||
}
|
||||
|
||||
func (rwt *rollingFileWriterTime) needsToRoll() (bool, error) {
|
||||
switch rwt.nameMode {
|
||||
case rollingNameModePostfix:
|
||||
if rwt.originalFileName+rollingLogHistoryDelimiter+time.Now().Format(rwt.timePattern) == rwt.fileName {
|
||||
return false, nil
|
||||
}
|
||||
case rollingNameModePrefix:
|
||||
if time.Now().Format(rwt.timePattern)+rollingLogHistoryDelimiter+rwt.originalFileName == rwt.fileName {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if rwt.interval == rollingIntervalAny {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
tprev, err := time.ParseInLocation(rwt.timePattern, rwt.getFileRollName(rwt.fileName), time.Local)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
diff := time.Now().Sub(tprev)
|
||||
switch rwt.interval {
|
||||
case rollingIntervalDaily:
|
||||
return diff >= 24*time.Hour, nil
|
||||
}
|
||||
return false, fmt.Errorf("unknown interval type: %d", rwt.interval)
|
||||
}
|
||||
|
||||
func (rwt *rollingFileWriterTime) isFileRollNameValid(rname string) bool {
|
||||
if len(rname) == 0 {
|
||||
return false
|
||||
}
|
||||
_, err := time.ParseInLocation(rwt.timePattern, rname, time.Local)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
type rollTimeFileTailsSlice struct {
|
||||
data []string
|
||||
pattern string
|
||||
}
|
||||
|
||||
func (p rollTimeFileTailsSlice) Len() int { return len(p.data) }
|
||||
|
||||
func (p rollTimeFileTailsSlice) Less(i, j int) bool {
|
||||
t1, _ := time.ParseInLocation(p.pattern, p.data[i], time.Local)
|
||||
t2, _ := time.ParseInLocation(p.pattern, p.data[j], time.Local)
|
||||
return t1.Before(t2)
|
||||
}
|
||||
|
||||
func (p rollTimeFileTailsSlice) Swap(i, j int) { p.data[i], p.data[j] = p.data[j], p.data[i] }
|
||||
|
||||
func (rwt *rollingFileWriterTime) sortFileRollNamesAsc(fs []string) ([]string, error) {
|
||||
ss := rollTimeFileTailsSlice{data: fs, pattern: rwt.timePattern}
|
||||
sort.Sort(ss)
|
||||
return ss.data, nil
|
||||
}
|
||||
|
||||
func (rwt *rollingFileWriterTime) getNewHistoryRollFileName(lastRollName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (rwt *rollingFileWriterTime) getCurrentModifiedFileName(originalFileName string, first bool) (string, error) {
|
||||
if first {
|
||||
history, err := rwt.getSortedLogHistory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(history) > 0 {
|
||||
return history[len(history)-1], nil
|
||||
}
|
||||
}
|
||||
|
||||
switch rwt.nameMode {
|
||||
case rollingNameModePostfix:
|
||||
return originalFileName + rollingLogHistoryDelimiter + time.Now().Format(rwt.timePattern), nil
|
||||
case rollingNameModePrefix:
|
||||
return time.Now().Format(rwt.timePattern) + rollingLogHistoryDelimiter + originalFileName, nil
|
||||
}
|
||||
return "", fmt.Errorf("Unknown rolling writer mode. Either postfix or prefix must be used")
|
||||
}
|
||||
|
||||
func (rwt *rollingFileWriterTime) String() string {
|
||||
return fmt.Sprintf("Rolling file writer (By TIME): filename: %s, archive: %s, archivefile: %s, maxInterval: %v, pattern: %s, maxRolls: %v",
|
||||
rwt.fileName,
|
||||
rollingArchiveTypesStringRepresentation[rwt.archiveType],
|
||||
rwt.archivePath,
|
||||
rwt.interval,
|
||||
rwt.timePattern,
|
||||
rwt.maxRolls)
|
||||
}
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this
|
||||
// list of conditions and the following disclaimer.
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
package seelog
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/smtp"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default subject phrase for sending emails.
|
||||
DefaultSubjectPhrase = "Diagnostic message from server: "
|
||||
|
||||
// Message subject pattern composed according to RFC 5321.
|
||||
rfc5321SubjectPattern = "From: %s <%s>\nSubject: %s\n\n"
|
||||
)
|
||||
|
||||
// smtpWriter is used to send emails via given SMTP-server.
|
||||
type smtpWriter struct {
|
||||
auth smtp.Auth
|
||||
hostName string
|
||||
hostPort string
|
||||
hostNameWithPort string
|
||||
senderAddress string
|
||||
senderName string
|
||||
recipientAddresses []string
|
||||
caCertDirPaths []string
|
||||
mailHeaders []string
|
||||
subject string
|
||||
}
|
||||
|
||||
// NewSMTPWriter returns a new SMTP-writer.
|
||||
func NewSMTPWriter(sa, sn string, ras []string, hn, hp, un, pwd string, cacdps []string, subj string, headers []string) *smtpWriter {
|
||||
return &smtpWriter{
|
||||
auth: smtp.PlainAuth("", un, pwd, hn),
|
||||
hostName: hn,
|
||||
hostPort: hp,
|
||||
hostNameWithPort: fmt.Sprintf("%s:%s", hn, hp),
|
||||
senderAddress: sa,
|
||||
senderName: sn,
|
||||
recipientAddresses: ras,
|
||||
caCertDirPaths: cacdps,
|
||||
subject: subj,
|
||||
mailHeaders: headers,
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMessage(senderAddr, senderName, subject string, body []byte, headers []string) []byte {
|
||||
headerLines := fmt.Sprintf(rfc5321SubjectPattern, senderName, senderAddr, subject)
|
||||
// Build header lines if configured.
|
||||
if headers != nil && len(headers) > 0 {
|
||||
headerLines += strings.Join(headers, "\n")
|
||||
headerLines += "\n"
|
||||
}
|
||||
return append([]byte(headerLines), body...)
|
||||
}
|
||||
|
||||
// getTLSConfig gets paths of PEM files with certificates,
|
||||
// host server name and tries to create an appropriate TLS.Config.
|
||||
func getTLSConfig(pemFileDirPaths []string, hostName string) (config *tls.Config, err error) {
|
||||
if pemFileDirPaths == nil || len(pemFileDirPaths) == 0 {
|
||||
err = errors.New("invalid PEM file paths")
|
||||
return
|
||||
}
|
||||
pemEncodedContent := []byte{}
|
||||
var (
|
||||
e error
|
||||
bytes []byte
|
||||
)
|
||||
// Create a file-filter-by-extension, set aside non-pem files.
|
||||
pemFilePathFilter := func(fp string) bool {
|
||||
if filepath.Ext(fp) == ".pem" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, pemFileDirPath := range pemFileDirPaths {
|
||||
pemFilePaths, err := getDirFilePaths(pemFileDirPath, pemFilePathFilter, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Put together all the PEM files to decode them as a whole byte slice.
|
||||
for _, pfp := range pemFilePaths {
|
||||
if bytes, e = ioutil.ReadFile(pfp); e == nil {
|
||||
pemEncodedContent = append(pemEncodedContent, bytes...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("cannot read file: %s: %s", pfp, e.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
config = &tls.Config{RootCAs: x509.NewCertPool(), ServerName: hostName}
|
||||
isAppended := config.RootCAs.AppendCertsFromPEM(pemEncodedContent)
|
||||
if !isAppended {
|
||||
// Extract this into a separate error.
|
||||
err = errors.New("invalid PEM content")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SendMail accepts TLS configuration, connects to the server at addr,
|
||||
// switches to TLS if possible, authenticates with mechanism a if possible,
|
||||
// and then sends an email from address from, to addresses to, with message msg.
|
||||
func sendMailWithTLSConfig(config *tls.Config, addr string, a smtp.Auth, from string, to []string, msg []byte) error {
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check if the server supports STARTTLS extension.
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
if err = c.StartTLS(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Check if the server supports AUTH extension and use given smtp.Auth.
|
||||
if a != nil {
|
||||
if isSupported, _ := c.Extension("AUTH"); isSupported {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Portion of code from the official smtp.SendMail function,
|
||||
// see http://golang.org/src/pkg/net/smtp/smtp.go.
|
||||
if err = c.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Write pushes a text message properly composed according to RFC 5321
|
||||
// to a post server, which sends it to the recipients.
|
||||
func (smtpw *smtpWriter) Write(data []byte) (int, error) {
|
||||
var err error
|
||||
|
||||
if smtpw.caCertDirPaths == nil {
|
||||
err = smtp.SendMail(
|
||||
smtpw.hostNameWithPort,
|
||||
smtpw.auth,
|
||||
smtpw.senderAddress,
|
||||
smtpw.recipientAddresses,
|
||||
prepareMessage(smtpw.senderAddress, smtpw.senderName, smtpw.subject, data, smtpw.mailHeaders),
|
||||
)
|
||||
} else {
|
||||
config, e := getTLSConfig(smtpw.caCertDirPaths, smtpw.hostName)
|
||||
if e != nil {
|
||||
return 0, e
|
||||
}
|
||||
err = sendMailWithTLSConfig(
|
||||
config,
|
||||
smtpw.hostNameWithPort,
|
||||
smtpw.auth,
|
||||
smtpw.senderAddress,
|
||||
smtpw.recipientAddresses,
|
||||
prepareMessage(smtpw.senderAddress, smtpw.senderName, smtpw.subject, data, smtpw.mailHeaders),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// Close closes down SMTP-connection.
|
||||
func (smtpw *smtpWriter) Close() error {
|
||||
// Do nothing as Write method opens and closes connection automatically.
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||
// when the code is not running on Google App Engine, compiled by GopherJS, and
|
||||
// "-tags safe" is not added to the go build command line. The "disableunsafe"
|
||||
// tag is deprecated and thus should not be used.
|
||||
// +build !js,!appengine,!safe,!disableunsafe
|
||||
|
||||
package spew
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// UnsafeDisabled is a build-time constant which specifies whether or
|
||||
// not access to the unsafe package is available.
|
||||
UnsafeDisabled = false
|
||||
|
||||
// ptrSize is the size of a pointer on the current arch.
|
||||
ptrSize = unsafe.Sizeof((*byte)(nil))
|
||||
)
|
||||
|
||||
var (
|
||||
// offsetPtr, offsetScalar, and offsetFlag are the offsets for the
|
||||
// internal reflect.Value fields. These values are valid before golang
|
||||
// commit ecccf07e7f9d which changed the format. The are also valid
|
||||
// after commit 82f48826c6c7 which changed the format again to mirror
|
||||
// the original format. Code in the init function updates these offsets
|
||||
// as necessary.
|
||||
offsetPtr = uintptr(ptrSize)
|
||||
offsetScalar = uintptr(0)
|
||||
offsetFlag = uintptr(ptrSize * 2)
|
||||
|
||||
// flagKindWidth and flagKindShift indicate various bits that the
|
||||
// reflect package uses internally to track kind information.
|
||||
//
|
||||
// flagRO indicates whether or not the value field of a reflect.Value is
|
||||
// read-only.
|
||||
//
|
||||
// flagIndir indicates whether the value field of a reflect.Value is
|
||||
// the actual data or a pointer to the data.
|
||||
//
|
||||
// These values are valid before golang commit 90a7c3c86944 which
|
||||
// changed their positions. Code in the init function updates these
|
||||
// flags as necessary.
|
||||
flagKindWidth = uintptr(5)
|
||||
flagKindShift = uintptr(flagKindWidth - 1)
|
||||
flagRO = uintptr(1 << 0)
|
||||
flagIndir = uintptr(1 << 1)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Older versions of reflect.Value stored small integers directly in the
|
||||
// ptr field (which is named val in the older versions). Versions
|
||||
// between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
|
||||
// scalar for this purpose which unfortunately came before the flag
|
||||
// field, so the offset of the flag field is different for those
|
||||
// versions.
|
||||
//
|
||||
// This code constructs a new reflect.Value from a known small integer
|
||||
// and checks if the size of the reflect.Value struct indicates it has
|
||||
// the scalar field. When it does, the offsets are updated accordingly.
|
||||
vv := reflect.ValueOf(0xf00)
|
||||
if unsafe.Sizeof(vv) == (ptrSize * 4) {
|
||||
offsetScalar = ptrSize * 2
|
||||
offsetFlag = ptrSize * 3
|
||||
}
|
||||
|
||||
// Commit 90a7c3c86944 changed the flag positions such that the low
|
||||
// order bits are the kind. This code extracts the kind from the flags
|
||||
// field and ensures it's the correct type. When it's not, the flag
|
||||
// order has been changed to the newer format, so the flags are updated
|
||||
// accordingly.
|
||||
upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
|
||||
upfv := *(*uintptr)(upf)
|
||||
flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift)
|
||||
if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) {
|
||||
flagKindShift = 0
|
||||
flagRO = 1 << 5
|
||||
flagIndir = 1 << 6
|
||||
|
||||
// Commit adf9b30e5594 modified the flags to separate the
|
||||
// flagRO flag into two bits which specifies whether or not the
|
||||
// field is embedded. This causes flagIndir to move over a bit
|
||||
// and means that flagRO is the combination of either of the
|
||||
// original flagRO bit and the new bit.
|
||||
//
|
||||
// This code detects the change by extracting what used to be
|
||||
// the indirect bit to ensure it's set. When it's not, the flag
|
||||
// order has been changed to the newer format, so the flags are
|
||||
// updated accordingly.
|
||||
if upfv&flagIndir == 0 {
|
||||
flagRO = 3 << 5
|
||||
flagIndir = 1 << 7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
|
||||
// the typical safety restrictions preventing access to unaddressable and
|
||||
// unexported data. It works by digging the raw pointer to the underlying
|
||||
// value out of the protected value and generating a new unprotected (unsafe)
|
||||
// reflect.Value to it.
|
||||
//
|
||||
// This allows us to check for implementations of the Stringer and error
|
||||
// interfaces to be used for pretty printing ordinarily unaddressable and
|
||||
// inaccessible values such as unexported struct fields.
|
||||
func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
|
||||
indirects := 1
|
||||
vt := v.Type()
|
||||
upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
|
||||
rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
|
||||
if rvf&flagIndir != 0 {
|
||||
vt = reflect.PtrTo(v.Type())
|
||||
indirects++
|
||||
} else if offsetScalar != 0 {
|
||||
// The value is in the scalar field when it's not one of the
|
||||
// reference types.
|
||||
switch vt.Kind() {
|
||||
case reflect.Uintptr:
|
||||
case reflect.Chan:
|
||||
case reflect.Func:
|
||||
case reflect.Map:
|
||||
case reflect.Ptr:
|
||||
case reflect.UnsafePointer:
|
||||
default:
|
||||
upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
|
||||
offsetScalar)
|
||||
}
|
||||
}
|
||||
|
||||
pv := reflect.NewAt(vt, upv)
|
||||
rv = pv
|
||||
for i := 0; i < indirects; i++ {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
// Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// NOTE: Due to the following build constraints, this file will only be compiled
|
||||
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||
// tag is deprecated and thus should not be used.
|
||||
// +build js appengine safe disableunsafe
|
||||
|
||||
package spew
|
||||
|
||||
import "reflect"
|
||||
|
||||
const (
|
||||
// UnsafeDisabled is a build-time constant which specifies whether or
|
||||
// not access to the unsafe package is available.
|
||||
UnsafeDisabled = true
|
||||
)
|
||||
|
||||
// unsafeReflectValue typically converts the passed reflect.Value into a one
|
||||
// that bypasses the typical safety restrictions preventing access to
|
||||
// unaddressable and unexported data. However, doing this relies on access to
|
||||
// the unsafe package. This is a stub version which simply returns the passed
|
||||
// reflect.Value when the unsafe package is not available.
|
||||
func unsafeReflectValue(v reflect.Value) reflect.Value {
|
||||
return v
|
||||
}
|
||||
|
|
@ -1,341 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Some constants in the form of bytes to avoid string overhead. This mirrors
|
||||
// the technique used in the fmt package.
|
||||
var (
|
||||
panicBytes = []byte("(PANIC=")
|
||||
plusBytes = []byte("+")
|
||||
iBytes = []byte("i")
|
||||
trueBytes = []byte("true")
|
||||
falseBytes = []byte("false")
|
||||
interfaceBytes = []byte("(interface {})")
|
||||
commaNewlineBytes = []byte(",\n")
|
||||
newlineBytes = []byte("\n")
|
||||
openBraceBytes = []byte("{")
|
||||
openBraceNewlineBytes = []byte("{\n")
|
||||
closeBraceBytes = []byte("}")
|
||||
asteriskBytes = []byte("*")
|
||||
colonBytes = []byte(":")
|
||||
colonSpaceBytes = []byte(": ")
|
||||
openParenBytes = []byte("(")
|
||||
closeParenBytes = []byte(")")
|
||||
spaceBytes = []byte(" ")
|
||||
pointerChainBytes = []byte("->")
|
||||
nilAngleBytes = []byte("<nil>")
|
||||
maxNewlineBytes = []byte("<max depth reached>\n")
|
||||
maxShortBytes = []byte("<max>")
|
||||
circularBytes = []byte("<already shown>")
|
||||
circularShortBytes = []byte("<shown>")
|
||||
invalidAngleBytes = []byte("<invalid>")
|
||||
openBracketBytes = []byte("[")
|
||||
closeBracketBytes = []byte("]")
|
||||
percentBytes = []byte("%")
|
||||
precisionBytes = []byte(".")
|
||||
openAngleBytes = []byte("<")
|
||||
closeAngleBytes = []byte(">")
|
||||
openMapBytes = []byte("map[")
|
||||
closeMapBytes = []byte("]")
|
||||
lenEqualsBytes = []byte("len=")
|
||||
capEqualsBytes = []byte("cap=")
|
||||
)
|
||||
|
||||
// hexDigits is used to map a decimal value to a hex digit.
|
||||
var hexDigits = "0123456789abcdef"
|
||||
|
||||
// catchPanic handles any panics that might occur during the handleMethods
|
||||
// calls.
|
||||
func catchPanic(w io.Writer, v reflect.Value) {
|
||||
if err := recover(); err != nil {
|
||||
w.Write(panicBytes)
|
||||
fmt.Fprintf(w, "%v", err)
|
||||
w.Write(closeParenBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// handleMethods attempts to call the Error and String methods on the underlying
|
||||
// type the passed reflect.Value represents and outputes the result to Writer w.
|
||||
//
|
||||
// It handles panics in any called methods by catching and displaying the error
|
||||
// as the formatted value.
|
||||
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
|
||||
// We need an interface to check if the type implements the error or
|
||||
// Stringer interface. However, the reflect package won't give us an
|
||||
// interface on certain things like unexported struct fields in order
|
||||
// to enforce visibility rules. We use unsafe, when it's available,
|
||||
// to bypass these restrictions since this package does not mutate the
|
||||
// values.
|
||||
if !v.CanInterface() {
|
||||
if UnsafeDisabled {
|
||||
return false
|
||||
}
|
||||
|
||||
v = unsafeReflectValue(v)
|
||||
}
|
||||
|
||||
// Choose whether or not to do error and Stringer interface lookups against
|
||||
// the base type or a pointer to the base type depending on settings.
|
||||
// Technically calling one of these methods with a pointer receiver can
|
||||
// mutate the value, however, types which choose to satisify an error or
|
||||
// Stringer interface with a pointer receiver should not be mutating their
|
||||
// state inside these interface methods.
|
||||
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
|
||||
v = unsafeReflectValue(v)
|
||||
}
|
||||
if v.CanAddr() {
|
||||
v = v.Addr()
|
||||
}
|
||||
|
||||
// Is it an error or Stringer?
|
||||
switch iface := v.Interface().(type) {
|
||||
case error:
|
||||
defer catchPanic(w, v)
|
||||
if cs.ContinueOnMethod {
|
||||
w.Write(openParenBytes)
|
||||
w.Write([]byte(iface.Error()))
|
||||
w.Write(closeParenBytes)
|
||||
w.Write(spaceBytes)
|
||||
return false
|
||||
}
|
||||
|
||||
w.Write([]byte(iface.Error()))
|
||||
return true
|
||||
|
||||
case fmt.Stringer:
|
||||
defer catchPanic(w, v)
|
||||
if cs.ContinueOnMethod {
|
||||
w.Write(openParenBytes)
|
||||
w.Write([]byte(iface.String()))
|
||||
w.Write(closeParenBytes)
|
||||
w.Write(spaceBytes)
|
||||
return false
|
||||
}
|
||||
w.Write([]byte(iface.String()))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// printBool outputs a boolean value as true or false to Writer w.
|
||||
func printBool(w io.Writer, val bool) {
|
||||
if val {
|
||||
w.Write(trueBytes)
|
||||
} else {
|
||||
w.Write(falseBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// printInt outputs a signed integer value to Writer w.
|
||||
func printInt(w io.Writer, val int64, base int) {
|
||||
w.Write([]byte(strconv.FormatInt(val, base)))
|
||||
}
|
||||
|
||||
// printUint outputs an unsigned integer value to Writer w.
|
||||
func printUint(w io.Writer, val uint64, base int) {
|
||||
w.Write([]byte(strconv.FormatUint(val, base)))
|
||||
}
|
||||
|
||||
// printFloat outputs a floating point value using the specified precision,
|
||||
// which is expected to be 32 or 64bit, to Writer w.
|
||||
func printFloat(w io.Writer, val float64, precision int) {
|
||||
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
|
||||
}
|
||||
|
||||
// printComplex outputs a complex value using the specified float precision
|
||||
// for the real and imaginary parts to Writer w.
|
||||
func printComplex(w io.Writer, c complex128, floatPrecision int) {
|
||||
r := real(c)
|
||||
w.Write(openParenBytes)
|
||||
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
|
||||
i := imag(c)
|
||||
if i >= 0 {
|
||||
w.Write(plusBytes)
|
||||
}
|
||||
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
|
||||
w.Write(iBytes)
|
||||
w.Write(closeParenBytes)
|
||||
}
|
||||
|
||||
// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x'
|
||||
// prefix to Writer w.
|
||||
func printHexPtr(w io.Writer, p uintptr) {
|
||||
// Null pointer.
|
||||
num := uint64(p)
|
||||
if num == 0 {
|
||||
w.Write(nilAngleBytes)
|
||||
return
|
||||
}
|
||||
|
||||
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
|
||||
buf := make([]byte, 18)
|
||||
|
||||
// It's simpler to construct the hex string right to left.
|
||||
base := uint64(16)
|
||||
i := len(buf) - 1
|
||||
for num >= base {
|
||||
buf[i] = hexDigits[num%base]
|
||||
num /= base
|
||||
i--
|
||||
}
|
||||
buf[i] = hexDigits[num]
|
||||
|
||||
// Add '0x' prefix.
|
||||
i--
|
||||
buf[i] = 'x'
|
||||
i--
|
||||
buf[i] = '0'
|
||||
|
||||
// Strip unused leading bytes.
|
||||
buf = buf[i:]
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
|
||||
// elements to be sorted.
|
||||
type valuesSorter struct {
|
||||
values []reflect.Value
|
||||
strings []string // either nil or same len and values
|
||||
cs *ConfigState
|
||||
}
|
||||
|
||||
// newValuesSorter initializes a valuesSorter instance, which holds a set of
|
||||
// surrogate keys on which the data should be sorted. It uses flags in
|
||||
// ConfigState to decide if and how to populate those surrogate keys.
|
||||
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
|
||||
vs := &valuesSorter{values: values, cs: cs}
|
||||
if canSortSimply(vs.values[0].Kind()) {
|
||||
return vs
|
||||
}
|
||||
if !cs.DisableMethods {
|
||||
vs.strings = make([]string, len(values))
|
||||
for i := range vs.values {
|
||||
b := bytes.Buffer{}
|
||||
if !handleMethods(cs, &b, vs.values[i]) {
|
||||
vs.strings = nil
|
||||
break
|
||||
}
|
||||
vs.strings[i] = b.String()
|
||||
}
|
||||
}
|
||||
if vs.strings == nil && cs.SpewKeys {
|
||||
vs.strings = make([]string, len(values))
|
||||
for i := range vs.values {
|
||||
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
|
||||
}
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
|
||||
// directly, or whether it should be considered for sorting by surrogate keys
|
||||
// (if the ConfigState allows it).
|
||||
func canSortSimply(kind reflect.Kind) bool {
|
||||
// This switch parallels valueSortLess, except for the default case.
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
return true
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||
return true
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||
return true
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
case reflect.String:
|
||||
return true
|
||||
case reflect.Uintptr:
|
||||
return true
|
||||
case reflect.Array:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Len returns the number of values in the slice. It is part of the
|
||||
// sort.Interface implementation.
|
||||
func (s *valuesSorter) Len() int {
|
||||
return len(s.values)
|
||||
}
|
||||
|
||||
// Swap swaps the values at the passed indices. It is part of the
|
||||
// sort.Interface implementation.
|
||||
func (s *valuesSorter) Swap(i, j int) {
|
||||
s.values[i], s.values[j] = s.values[j], s.values[i]
|
||||
if s.strings != nil {
|
||||
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
|
||||
}
|
||||
}
|
||||
|
||||
// valueSortLess returns whether the first value should sort before the second
|
||||
// value. It is used by valueSorter.Less as part of the sort.Interface
|
||||
// implementation.
|
||||
func valueSortLess(a, b reflect.Value) bool {
|
||||
switch a.Kind() {
|
||||
case reflect.Bool:
|
||||
return !a.Bool() && b.Bool()
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||
return a.Int() < b.Int()
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||
return a.Uint() < b.Uint()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return a.Float() < b.Float()
|
||||
case reflect.String:
|
||||
return a.String() < b.String()
|
||||
case reflect.Uintptr:
|
||||
return a.Uint() < b.Uint()
|
||||
case reflect.Array:
|
||||
// Compare the contents of both arrays.
|
||||
l := a.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
av := a.Index(i)
|
||||
bv := b.Index(i)
|
||||
if av.Interface() == bv.Interface() {
|
||||
continue
|
||||
}
|
||||
return valueSortLess(av, bv)
|
||||
}
|
||||
}
|
||||
return a.String() < b.String()
|
||||
}
|
||||
|
||||
// Less returns whether the value at index i should sort before the
|
||||
// value at index j. It is part of the sort.Interface implementation.
|
||||
func (s *valuesSorter) Less(i, j int) bool {
|
||||
if s.strings == nil {
|
||||
return valueSortLess(s.values[i], s.values[j])
|
||||
}
|
||||
return s.strings[i] < s.strings[j]
|
||||
}
|
||||
|
||||
// sortValues is a sort function that handles both native types and any type that
|
||||
// can be converted to error or Stringer. Other inputs are sorted according to
|
||||
// their Value.String() value to ensure display stability.
|
||||
func sortValues(values []reflect.Value, cs *ConfigState) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
sort.Sort(newValuesSorter(values, cs))
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ConfigState houses the configuration options used by spew to format and
|
||||
// display values. There is a global instance, Config, that is used to control
|
||||
// all top-level Formatter and Dump functionality. Each ConfigState instance
|
||||
// provides methods equivalent to the top-level functions.
|
||||
//
|
||||
// The zero value for ConfigState provides no indentation. You would typically
|
||||
// want to set it to a space or a tab.
|
||||
//
|
||||
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
|
||||
// with default settings. See the documentation of NewDefaultConfig for default
|
||||
// values.
|
||||
type ConfigState struct {
|
||||
// Indent specifies the string to use for each indentation level. The
|
||||
// global config instance that all top-level functions use set this to a
|
||||
// single space by default. If you would like more indentation, you might
|
||||
// set this to a tab with "\t" or perhaps two spaces with " ".
|
||||
Indent string
|
||||
|
||||
// MaxDepth controls the maximum number of levels to descend into nested
|
||||
// data structures. The default, 0, means there is no limit.
|
||||
//
|
||||
// NOTE: Circular data structures are properly detected, so it is not
|
||||
// necessary to set this value unless you specifically want to limit deeply
|
||||
// nested data structures.
|
||||
MaxDepth int
|
||||
|
||||
// DisableMethods specifies whether or not error and Stringer interfaces are
|
||||
// invoked for types that implement them.
|
||||
DisableMethods bool
|
||||
|
||||
// DisablePointerMethods specifies whether or not to check for and invoke
|
||||
// error and Stringer interfaces on types which only accept a pointer
|
||||
// receiver when the current type is not a pointer.
|
||||
//
|
||||
// NOTE: This might be an unsafe action since calling one of these methods
|
||||
// with a pointer receiver could technically mutate the value, however,
|
||||
// in practice, types which choose to satisify an error or Stringer
|
||||
// interface with a pointer receiver should not be mutating their state
|
||||
// inside these interface methods. As a result, this option relies on
|
||||
// access to the unsafe package, so it will not have any effect when
|
||||
// running in environments without access to the unsafe package such as
|
||||
// Google App Engine or with the "safe" build tag specified.
|
||||
DisablePointerMethods bool
|
||||
|
||||
// DisablePointerAddresses specifies whether to disable the printing of
|
||||
// pointer addresses. This is useful when diffing data structures in tests.
|
||||
DisablePointerAddresses bool
|
||||
|
||||
// DisableCapacities specifies whether to disable the printing of capacities
|
||||
// for arrays, slices, maps and channels. This is useful when diffing
|
||||
// data structures in tests.
|
||||
DisableCapacities bool
|
||||
|
||||
// ContinueOnMethod specifies whether or not recursion should continue once
|
||||
// a custom error or Stringer interface is invoked. The default, false,
|
||||
// means it will print the results of invoking the custom error or Stringer
|
||||
// interface and return immediately instead of continuing to recurse into
|
||||
// the internals of the data type.
|
||||
//
|
||||
// NOTE: This flag does not have any effect if method invocation is disabled
|
||||
// via the DisableMethods or DisablePointerMethods options.
|
||||
ContinueOnMethod bool
|
||||
|
||||
// SortKeys specifies map keys should be sorted before being printed. Use
|
||||
// this to have a more deterministic, diffable output. Note that only
|
||||
// native types (bool, int, uint, floats, uintptr and string) and types
|
||||
// that support the error or Stringer interfaces (if methods are
|
||||
// enabled) are supported, with other types sorted according to the
|
||||
// reflect.Value.String() output which guarantees display stability.
|
||||
SortKeys bool
|
||||
|
||||
// SpewKeys specifies that, as a last resort attempt, map keys should
|
||||
// be spewed to strings and sorted by those strings. This is only
|
||||
// considered if SortKeys is true.
|
||||
SpewKeys bool
|
||||
}
|
||||
|
||||
// Config is the active configuration of the top-level functions.
|
||||
// The configuration can be changed by modifying the contents of spew.Config.
|
||||
var Config = ConfigState{Indent: " "}
|
||||
|
||||
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||
// the formatted string as a value that satisfies error. See NewFormatter
|
||||
// for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
|
||||
return fmt.Errorf(format, c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||
// the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprint(w, c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||
// the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprintf(w, format, c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
||||
// passed with a Formatter interface returned by c.NewFormatter. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprintln(w, c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||
// the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
|
||||
return fmt.Print(c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||
// the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
|
||||
return fmt.Printf(format, c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||
// the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
|
||||
return fmt.Println(c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||
// the resulting string. See NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Sprint(a ...interface{}) string {
|
||||
return fmt.Sprint(c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
||||
// passed with a Formatter interface returned by c.NewFormatter. It returns
|
||||
// the resulting string. See NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
|
||||
return fmt.Sprintf(format, c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
||||
// were passed with a Formatter interface returned by c.NewFormatter. It
|
||||
// returns the resulting string. See NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
|
||||
func (c *ConfigState) Sprintln(a ...interface{}) string {
|
||||
return fmt.Sprintln(c.convertArgs(a)...)
|
||||
}
|
||||
|
||||
/*
|
||||
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
||||
interface. As a result, it integrates cleanly with standard fmt package
|
||||
printing functions. The formatter is useful for inline printing of smaller data
|
||||
types similar to the standard %v format specifier.
|
||||
|
||||
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
|
||||
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||
the width and precision arguments (however they will still work on the format
|
||||
specifiers not handled by the custom formatter).
|
||||
|
||||
Typically this function shouldn't be called directly. It is much easier to make
|
||||
use of the custom formatter by calling one of the convenience functions such as
|
||||
c.Printf, c.Println, or c.Printf.
|
||||
*/
|
||||
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
|
||||
return newFormatter(c, v)
|
||||
}
|
||||
|
||||
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
||||
// exactly the same as Dump.
|
||||
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
|
||||
fdump(c, w, a...)
|
||||
}
|
||||
|
||||
/*
|
||||
Dump displays the passed parameters to standard out with newlines, customizable
|
||||
indentation, and additional debug information such as complete types and all
|
||||
pointer addresses used to indirect to the final value. It provides the
|
||||
following features over the built-in printing facilities provided by the fmt
|
||||
package:
|
||||
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
|
||||
The configuration options are controlled by modifying the public members
|
||||
of c. See ConfigState for options documentation.
|
||||
|
||||
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
||||
get the formatted result as a string.
|
||||
*/
|
||||
func (c *ConfigState) Dump(a ...interface{}) {
|
||||
fdump(c, os.Stdout, a...)
|
||||
}
|
||||
|
||||
// Sdump returns a string with the passed arguments formatted exactly the same
|
||||
// as Dump.
|
||||
func (c *ConfigState) Sdump(a ...interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
fdump(c, &buf, a...)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// convertArgs accepts a slice of arguments and returns a slice of the same
|
||||
// length with each argument converted to a spew Formatter interface using
|
||||
// the ConfigState associated with s.
|
||||
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
|
||||
formatters = make([]interface{}, len(args))
|
||||
for index, arg := range args {
|
||||
formatters[index] = newFormatter(c, arg)
|
||||
}
|
||||
return formatters
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a ConfigState with the following default settings.
|
||||
//
|
||||
// Indent: " "
|
||||
// MaxDepth: 0
|
||||
// DisableMethods: false
|
||||
// DisablePointerMethods: false
|
||||
// ContinueOnMethod: false
|
||||
// SortKeys: false
|
||||
func NewDefaultConfig() *ConfigState {
|
||||
return &ConfigState{Indent: " "}
|
||||
}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
Package spew implements a deep pretty printer for Go data structures to aid in
|
||||
debugging.
|
||||
|
||||
A quick overview of the additional features spew provides over the built-in
|
||||
printing facilities for Go data types are as follows:
|
||||
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output (only when using
|
||||
Dump style)
|
||||
|
||||
There are two different approaches spew allows for dumping Go data structures:
|
||||
|
||||
* Dump style which prints with newlines, customizable indentation,
|
||||
and additional debug information such as types and all pointer addresses
|
||||
used to indirect to the final value
|
||||
* A custom Formatter interface that integrates cleanly with the standard fmt
|
||||
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
||||
similar to the default %v while providing the additional functionality
|
||||
outlined above and passing unsupported format verbs such as %x and %q
|
||||
along to fmt
|
||||
|
||||
Quick Start
|
||||
|
||||
This section demonstrates how to quickly get started with spew. See the
|
||||
sections below for further details on formatting and configuration options.
|
||||
|
||||
To dump a variable with full newlines, indentation, type, and pointer
|
||||
information use Dump, Fdump, or Sdump:
|
||||
spew.Dump(myVar1, myVar2, ...)
|
||||
spew.Fdump(someWriter, myVar1, myVar2, ...)
|
||||
str := spew.Sdump(myVar1, myVar2, ...)
|
||||
|
||||
Alternatively, if you would prefer to use format strings with a compacted inline
|
||||
printing style, use the convenience wrappers Printf, Fprintf, etc with
|
||||
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
|
||||
%#+v (adds types and pointer addresses):
|
||||
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
|
||||
Configuration Options
|
||||
|
||||
Configuration of spew is handled by fields in the ConfigState type. For
|
||||
convenience, all of the top-level functions use a global state available
|
||||
via the spew.Config global.
|
||||
|
||||
It is also possible to create a ConfigState instance that provides methods
|
||||
equivalent to the top-level functions. This allows concurrent configuration
|
||||
options. See the ConfigState documentation for more details.
|
||||
|
||||
The following configuration options are available:
|
||||
* Indent
|
||||
String to use for each indentation level for Dump functions.
|
||||
It is a single space by default. A popular alternative is "\t".
|
||||
|
||||
* MaxDepth
|
||||
Maximum number of levels to descend into nested data structures.
|
||||
There is no limit by default.
|
||||
|
||||
* DisableMethods
|
||||
Disables invocation of error and Stringer interface methods.
|
||||
Method invocation is enabled by default.
|
||||
|
||||
* DisablePointerMethods
|
||||
Disables invocation of error and Stringer interface methods on types
|
||||
which only accept pointer receivers from non-pointer variables.
|
||||
Pointer method invocation is enabled by default.
|
||||
|
||||
* DisablePointerAddresses
|
||||
DisablePointerAddresses specifies whether to disable the printing of
|
||||
pointer addresses. This is useful when diffing data structures in tests.
|
||||
|
||||
* DisableCapacities
|
||||
DisableCapacities specifies whether to disable the printing of
|
||||
capacities for arrays, slices, maps and channels. This is useful when
|
||||
diffing data structures in tests.
|
||||
|
||||
* ContinueOnMethod
|
||||
Enables recursion into types after invoking error and Stringer interface
|
||||
methods. Recursion after method invocation is disabled by default.
|
||||
|
||||
* SortKeys
|
||||
Specifies map keys should be sorted before being printed. Use
|
||||
this to have a more deterministic, diffable output. Note that
|
||||
only native types (bool, int, uint, floats, uintptr and string)
|
||||
and types which implement error or Stringer interfaces are
|
||||
supported with other types sorted according to the
|
||||
reflect.Value.String() output which guarantees display
|
||||
stability. Natural map order is used by default.
|
||||
|
||||
* SpewKeys
|
||||
Specifies that, as a last resort attempt, map keys should be
|
||||
spewed to strings and sorted by those strings. This is only
|
||||
considered if SortKeys is true.
|
||||
|
||||
Dump Usage
|
||||
|
||||
Simply call spew.Dump with a list of variables you want to dump:
|
||||
|
||||
spew.Dump(myVar1, myVar2, ...)
|
||||
|
||||
You may also call spew.Fdump if you would prefer to output to an arbitrary
|
||||
io.Writer. For example, to dump to standard error:
|
||||
|
||||
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
|
||||
|
||||
A third option is to call spew.Sdump to get the formatted output as a string:
|
||||
|
||||
str := spew.Sdump(myVar1, myVar2, ...)
|
||||
|
||||
Sample Dump Output
|
||||
|
||||
See the Dump example for details on the setup of the types and variables being
|
||||
shown here.
|
||||
|
||||
(main.Foo) {
|
||||
unexportedField: (*main.Bar)(0xf84002e210)({
|
||||
flag: (main.Flag) flagTwo,
|
||||
data: (uintptr) <nil>
|
||||
}),
|
||||
ExportedField: (map[interface {}]interface {}) (len=1) {
|
||||
(string) (len=3) "one": (bool) true
|
||||
}
|
||||
}
|
||||
|
||||
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
|
||||
command as shown.
|
||||
([]uint8) (len=32 cap=32) {
|
||||
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||
00000020 31 32 |12|
|
||||
}
|
||||
|
||||
Custom Formatter
|
||||
|
||||
Spew provides a custom formatter that implements the fmt.Formatter interface
|
||||
so that it integrates cleanly with standard fmt package printing functions. The
|
||||
formatter is useful for inline printing of smaller data types similar to the
|
||||
standard %v format specifier.
|
||||
|
||||
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
||||
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||
the width and precision arguments (however they will still work on the format
|
||||
specifiers not handled by the custom formatter).
|
||||
|
||||
Custom Formatter Usage
|
||||
|
||||
The simplest way to make use of the spew custom formatter is to call one of the
|
||||
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
|
||||
functions have syntax you are most likely already familiar with:
|
||||
|
||||
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
spew.Println(myVar, myVar2)
|
||||
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
|
||||
See the Index for the full list convenience functions.
|
||||
|
||||
Sample Formatter Output
|
||||
|
||||
Double pointer to a uint8:
|
||||
%v: <**>5
|
||||
%+v: <**>(0xf8400420d0->0xf8400420c8)5
|
||||
%#v: (**uint8)5
|
||||
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
|
||||
|
||||
Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||
%v: <*>{1 <*><shown>}
|
||||
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
|
||||
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
|
||||
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
|
||||
|
||||
See the Printf example for details on the setup of variables being shown
|
||||
here.
|
||||
|
||||
Errors
|
||||
|
||||
Since it is possible for custom Stringer/error interfaces to panic, spew
|
||||
detects them and handles them internally by printing the panic information
|
||||
inline with the output. Since spew is intended to provide deep pretty printing
|
||||
capabilities on structures, it intentionally does not return any errors.
|
||||
*/
|
||||
package spew
|
||||
|
|
@ -1,509 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// uint8Type is a reflect.Type representing a uint8. It is used to
|
||||
// convert cgo types to uint8 slices for hexdumping.
|
||||
uint8Type = reflect.TypeOf(uint8(0))
|
||||
|
||||
// cCharRE is a regular expression that matches a cgo char.
|
||||
// It is used to detect character arrays to hexdump them.
|
||||
cCharRE = regexp.MustCompile("^.*\\._Ctype_char$")
|
||||
|
||||
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
|
||||
// char. It is used to detect unsigned character arrays to hexdump
|
||||
// them.
|
||||
cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$")
|
||||
|
||||
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
|
||||
// It is used to detect uint8_t arrays to hexdump them.
|
||||
cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$")
|
||||
)
|
||||
|
||||
// dumpState contains information about the state of a dump operation.
|
||||
type dumpState struct {
|
||||
w io.Writer
|
||||
depth int
|
||||
pointers map[uintptr]int
|
||||
ignoreNextType bool
|
||||
ignoreNextIndent bool
|
||||
cs *ConfigState
|
||||
}
|
||||
|
||||
// indent performs indentation according to the depth level and cs.Indent
|
||||
// option.
|
||||
func (d *dumpState) indent() {
|
||||
if d.ignoreNextIndent {
|
||||
d.ignoreNextIndent = false
|
||||
return
|
||||
}
|
||||
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
|
||||
}
|
||||
|
||||
// unpackValue returns values inside of non-nil interfaces when possible.
|
||||
// This is useful for data types like structs, arrays, slices, and maps which
|
||||
// can contain varying types packed inside an interface.
|
||||
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
|
||||
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||
v = v.Elem()
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// dumpPtr handles formatting of pointers by indirecting them as necessary.
|
||||
func (d *dumpState) dumpPtr(v reflect.Value) {
|
||||
// Remove pointers at or below the current depth from map used to detect
|
||||
// circular refs.
|
||||
for k, depth := range d.pointers {
|
||||
if depth >= d.depth {
|
||||
delete(d.pointers, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Keep list of all dereferenced pointers to show later.
|
||||
pointerChain := make([]uintptr, 0)
|
||||
|
||||
// Figure out how many levels of indirection there are by dereferencing
|
||||
// pointers and unpacking interfaces down the chain while detecting circular
|
||||
// references.
|
||||
nilFound := false
|
||||
cycleFound := false
|
||||
indirects := 0
|
||||
ve := v
|
||||
for ve.Kind() == reflect.Ptr {
|
||||
if ve.IsNil() {
|
||||
nilFound = true
|
||||
break
|
||||
}
|
||||
indirects++
|
||||
addr := ve.Pointer()
|
||||
pointerChain = append(pointerChain, addr)
|
||||
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
|
||||
cycleFound = true
|
||||
indirects--
|
||||
break
|
||||
}
|
||||
d.pointers[addr] = d.depth
|
||||
|
||||
ve = ve.Elem()
|
||||
if ve.Kind() == reflect.Interface {
|
||||
if ve.IsNil() {
|
||||
nilFound = true
|
||||
break
|
||||
}
|
||||
ve = ve.Elem()
|
||||
}
|
||||
}
|
||||
|
||||
// Display type information.
|
||||
d.w.Write(openParenBytes)
|
||||
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
|
||||
d.w.Write([]byte(ve.Type().String()))
|
||||
d.w.Write(closeParenBytes)
|
||||
|
||||
// Display pointer information.
|
||||
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
|
||||
d.w.Write(openParenBytes)
|
||||
for i, addr := range pointerChain {
|
||||
if i > 0 {
|
||||
d.w.Write(pointerChainBytes)
|
||||
}
|
||||
printHexPtr(d.w, addr)
|
||||
}
|
||||
d.w.Write(closeParenBytes)
|
||||
}
|
||||
|
||||
// Display dereferenced value.
|
||||
d.w.Write(openParenBytes)
|
||||
switch {
|
||||
case nilFound == true:
|
||||
d.w.Write(nilAngleBytes)
|
||||
|
||||
case cycleFound == true:
|
||||
d.w.Write(circularBytes)
|
||||
|
||||
default:
|
||||
d.ignoreNextType = true
|
||||
d.dump(ve)
|
||||
}
|
||||
d.w.Write(closeParenBytes)
|
||||
}
|
||||
|
||||
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
|
||||
// reflection) arrays and slices are dumped in hexdump -C fashion.
|
||||
func (d *dumpState) dumpSlice(v reflect.Value) {
|
||||
// Determine whether this type should be hex dumped or not. Also,
|
||||
// for types which should be hexdumped, try to use the underlying data
|
||||
// first, then fall back to trying to convert them to a uint8 slice.
|
||||
var buf []uint8
|
||||
doConvert := false
|
||||
doHexDump := false
|
||||
numEntries := v.Len()
|
||||
if numEntries > 0 {
|
||||
vt := v.Index(0).Type()
|
||||
vts := vt.String()
|
||||
switch {
|
||||
// C types that need to be converted.
|
||||
case cCharRE.MatchString(vts):
|
||||
fallthrough
|
||||
case cUnsignedCharRE.MatchString(vts):
|
||||
fallthrough
|
||||
case cUint8tCharRE.MatchString(vts):
|
||||
doConvert = true
|
||||
|
||||
// Try to use existing uint8 slices and fall back to converting
|
||||
// and copying if that fails.
|
||||
case vt.Kind() == reflect.Uint8:
|
||||
// We need an addressable interface to convert the type
|
||||
// to a byte slice. However, the reflect package won't
|
||||
// give us an interface on certain things like
|
||||
// unexported struct fields in order to enforce
|
||||
// visibility rules. We use unsafe, when available, to
|
||||
// bypass these restrictions since this package does not
|
||||
// mutate the values.
|
||||
vs := v
|
||||
if !vs.CanInterface() || !vs.CanAddr() {
|
||||
vs = unsafeReflectValue(vs)
|
||||
}
|
||||
if !UnsafeDisabled {
|
||||
vs = vs.Slice(0, numEntries)
|
||||
|
||||
// Use the existing uint8 slice if it can be
|
||||
// type asserted.
|
||||
iface := vs.Interface()
|
||||
if slice, ok := iface.([]uint8); ok {
|
||||
buf = slice
|
||||
doHexDump = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// The underlying data needs to be converted if it can't
|
||||
// be type asserted to a uint8 slice.
|
||||
doConvert = true
|
||||
}
|
||||
|
||||
// Copy and convert the underlying type if needed.
|
||||
if doConvert && vt.ConvertibleTo(uint8Type) {
|
||||
// Convert and copy each element into a uint8 byte
|
||||
// slice.
|
||||
buf = make([]uint8, numEntries)
|
||||
for i := 0; i < numEntries; i++ {
|
||||
vv := v.Index(i)
|
||||
buf[i] = uint8(vv.Convert(uint8Type).Uint())
|
||||
}
|
||||
doHexDump = true
|
||||
}
|
||||
}
|
||||
|
||||
// Hexdump the entire slice as needed.
|
||||
if doHexDump {
|
||||
indent := strings.Repeat(d.cs.Indent, d.depth)
|
||||
str := indent + hex.Dump(buf)
|
||||
str = strings.Replace(str, "\n", "\n"+indent, -1)
|
||||
str = strings.TrimRight(str, d.cs.Indent)
|
||||
d.w.Write([]byte(str))
|
||||
return
|
||||
}
|
||||
|
||||
// Recursively call dump for each item.
|
||||
for i := 0; i < numEntries; i++ {
|
||||
d.dump(d.unpackValue(v.Index(i)))
|
||||
if i < (numEntries - 1) {
|
||||
d.w.Write(commaNewlineBytes)
|
||||
} else {
|
||||
d.w.Write(newlineBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dump is the main workhorse for dumping a value. It uses the passed reflect
|
||||
// value to figure out what kind of object we are dealing with and formats it
|
||||
// appropriately. It is a recursive function, however circular data structures
|
||||
// are detected and handled properly.
|
||||
func (d *dumpState) dump(v reflect.Value) {
|
||||
// Handle invalid reflect values immediately.
|
||||
kind := v.Kind()
|
||||
if kind == reflect.Invalid {
|
||||
d.w.Write(invalidAngleBytes)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle pointers specially.
|
||||
if kind == reflect.Ptr {
|
||||
d.indent()
|
||||
d.dumpPtr(v)
|
||||
return
|
||||
}
|
||||
|
||||
// Print type information unless already handled elsewhere.
|
||||
if !d.ignoreNextType {
|
||||
d.indent()
|
||||
d.w.Write(openParenBytes)
|
||||
d.w.Write([]byte(v.Type().String()))
|
||||
d.w.Write(closeParenBytes)
|
||||
d.w.Write(spaceBytes)
|
||||
}
|
||||
d.ignoreNextType = false
|
||||
|
||||
// Display length and capacity if the built-in len and cap functions
|
||||
// work with the value's kind and the len/cap itself is non-zero.
|
||||
valueLen, valueCap := 0, 0
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.Chan:
|
||||
valueLen, valueCap = v.Len(), v.Cap()
|
||||
case reflect.Map, reflect.String:
|
||||
valueLen = v.Len()
|
||||
}
|
||||
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
|
||||
d.w.Write(openParenBytes)
|
||||
if valueLen != 0 {
|
||||
d.w.Write(lenEqualsBytes)
|
||||
printInt(d.w, int64(valueLen), 10)
|
||||
}
|
||||
if !d.cs.DisableCapacities && valueCap != 0 {
|
||||
if valueLen != 0 {
|
||||
d.w.Write(spaceBytes)
|
||||
}
|
||||
d.w.Write(capEqualsBytes)
|
||||
printInt(d.w, int64(valueCap), 10)
|
||||
}
|
||||
d.w.Write(closeParenBytes)
|
||||
d.w.Write(spaceBytes)
|
||||
}
|
||||
|
||||
// Call Stringer/error interfaces if they exist and the handle methods flag
|
||||
// is enabled
|
||||
if !d.cs.DisableMethods {
|
||||
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
||||
if handled := handleMethods(d.cs, d.w, v); handled {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case reflect.Invalid:
|
||||
// Do nothing. We should never get here since invalid has already
|
||||
// been handled above.
|
||||
|
||||
case reflect.Bool:
|
||||
printBool(d.w, v.Bool())
|
||||
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||
printInt(d.w, v.Int(), 10)
|
||||
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||
printUint(d.w, v.Uint(), 10)
|
||||
|
||||
case reflect.Float32:
|
||||
printFloat(d.w, v.Float(), 32)
|
||||
|
||||
case reflect.Float64:
|
||||
printFloat(d.w, v.Float(), 64)
|
||||
|
||||
case reflect.Complex64:
|
||||
printComplex(d.w, v.Complex(), 32)
|
||||
|
||||
case reflect.Complex128:
|
||||
printComplex(d.w, v.Complex(), 64)
|
||||
|
||||
case reflect.Slice:
|
||||
if v.IsNil() {
|
||||
d.w.Write(nilAngleBytes)
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case reflect.Array:
|
||||
d.w.Write(openBraceNewlineBytes)
|
||||
d.depth++
|
||||
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||
d.indent()
|
||||
d.w.Write(maxNewlineBytes)
|
||||
} else {
|
||||
d.dumpSlice(v)
|
||||
}
|
||||
d.depth--
|
||||
d.indent()
|
||||
d.w.Write(closeBraceBytes)
|
||||
|
||||
case reflect.String:
|
||||
d.w.Write([]byte(strconv.Quote(v.String())))
|
||||
|
||||
case reflect.Interface:
|
||||
// The only time we should get here is for nil interfaces due to
|
||||
// unpackValue calls.
|
||||
if v.IsNil() {
|
||||
d.w.Write(nilAngleBytes)
|
||||
}
|
||||
|
||||
case reflect.Ptr:
|
||||
// Do nothing. We should never get here since pointers have already
|
||||
// been handled above.
|
||||
|
||||
case reflect.Map:
|
||||
// nil maps should be indicated as different than empty maps
|
||||
if v.IsNil() {
|
||||
d.w.Write(nilAngleBytes)
|
||||
break
|
||||
}
|
||||
|
||||
d.w.Write(openBraceNewlineBytes)
|
||||
d.depth++
|
||||
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||
d.indent()
|
||||
d.w.Write(maxNewlineBytes)
|
||||
} else {
|
||||
numEntries := v.Len()
|
||||
keys := v.MapKeys()
|
||||
if d.cs.SortKeys {
|
||||
sortValues(keys, d.cs)
|
||||
}
|
||||
for i, key := range keys {
|
||||
d.dump(d.unpackValue(key))
|
||||
d.w.Write(colonSpaceBytes)
|
||||
d.ignoreNextIndent = true
|
||||
d.dump(d.unpackValue(v.MapIndex(key)))
|
||||
if i < (numEntries - 1) {
|
||||
d.w.Write(commaNewlineBytes)
|
||||
} else {
|
||||
d.w.Write(newlineBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
d.depth--
|
||||
d.indent()
|
||||
d.w.Write(closeBraceBytes)
|
||||
|
||||
case reflect.Struct:
|
||||
d.w.Write(openBraceNewlineBytes)
|
||||
d.depth++
|
||||
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
|
||||
d.indent()
|
||||
d.w.Write(maxNewlineBytes)
|
||||
} else {
|
||||
vt := v.Type()
|
||||
numFields := v.NumField()
|
||||
for i := 0; i < numFields; i++ {
|
||||
d.indent()
|
||||
vtf := vt.Field(i)
|
||||
d.w.Write([]byte(vtf.Name))
|
||||
d.w.Write(colonSpaceBytes)
|
||||
d.ignoreNextIndent = true
|
||||
d.dump(d.unpackValue(v.Field(i)))
|
||||
if i < (numFields - 1) {
|
||||
d.w.Write(commaNewlineBytes)
|
||||
} else {
|
||||
d.w.Write(newlineBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
d.depth--
|
||||
d.indent()
|
||||
d.w.Write(closeBraceBytes)
|
||||
|
||||
case reflect.Uintptr:
|
||||
printHexPtr(d.w, uintptr(v.Uint()))
|
||||
|
||||
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||
printHexPtr(d.w, v.Pointer())
|
||||
|
||||
// There were not any other types at the time this code was written, but
|
||||
// fall back to letting the default fmt package handle it in case any new
|
||||
// types are added.
|
||||
default:
|
||||
if v.CanInterface() {
|
||||
fmt.Fprintf(d.w, "%v", v.Interface())
|
||||
} else {
|
||||
fmt.Fprintf(d.w, "%v", v.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fdump is a helper function to consolidate the logic from the various public
|
||||
// methods which take varying writers and config states.
|
||||
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
|
||||
for _, arg := range a {
|
||||
if arg == nil {
|
||||
w.Write(interfaceBytes)
|
||||
w.Write(spaceBytes)
|
||||
w.Write(nilAngleBytes)
|
||||
w.Write(newlineBytes)
|
||||
continue
|
||||
}
|
||||
|
||||
d := dumpState{w: w, cs: cs}
|
||||
d.pointers = make(map[uintptr]int)
|
||||
d.dump(reflect.ValueOf(arg))
|
||||
d.w.Write(newlineBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// Fdump formats and displays the passed arguments to io.Writer w. It formats
|
||||
// exactly the same as Dump.
|
||||
func Fdump(w io.Writer, a ...interface{}) {
|
||||
fdump(&Config, w, a...)
|
||||
}
|
||||
|
||||
// Sdump returns a string with the passed arguments formatted exactly the same
|
||||
// as Dump.
|
||||
func Sdump(a ...interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
fdump(&Config, &buf, a...)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
/*
|
||||
Dump displays the passed parameters to standard out with newlines, customizable
|
||||
indentation, and additional debug information such as complete types and all
|
||||
pointer addresses used to indirect to the final value. It provides the
|
||||
following features over the built-in printing facilities provided by the fmt
|
||||
package:
|
||||
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
|
||||
The configuration options are controlled by an exported package global,
|
||||
spew.Config. See ConfigState for options documentation.
|
||||
|
||||
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
|
||||
get the formatted result as a string.
|
||||
*/
|
||||
func Dump(a ...interface{}) {
|
||||
fdump(&Config, os.Stdout, a...)
|
||||
}
|
||||
|
|
@ -1,419 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// supportedFlags is a list of all the character flags supported by fmt package.
|
||||
const supportedFlags = "0-+# "
|
||||
|
||||
// formatState implements the fmt.Formatter interface and contains information
|
||||
// about the state of a formatting operation. The NewFormatter function can
|
||||
// be used to get a new Formatter which can be used directly as arguments
|
||||
// in standard fmt package printing calls.
|
||||
type formatState struct {
|
||||
value interface{}
|
||||
fs fmt.State
|
||||
depth int
|
||||
pointers map[uintptr]int
|
||||
ignoreNextType bool
|
||||
cs *ConfigState
|
||||
}
|
||||
|
||||
// buildDefaultFormat recreates the original format string without precision
|
||||
// and width information to pass in to fmt.Sprintf in the case of an
|
||||
// unrecognized type. Unless new types are added to the language, this
|
||||
// function won't ever be called.
|
||||
func (f *formatState) buildDefaultFormat() (format string) {
|
||||
buf := bytes.NewBuffer(percentBytes)
|
||||
|
||||
for _, flag := range supportedFlags {
|
||||
if f.fs.Flag(int(flag)) {
|
||||
buf.WriteRune(flag)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteRune('v')
|
||||
|
||||
format = buf.String()
|
||||
return format
|
||||
}
|
||||
|
||||
// constructOrigFormat recreates the original format string including precision
|
||||
// and width information to pass along to the standard fmt package. This allows
|
||||
// automatic deferral of all format strings this package doesn't support.
|
||||
func (f *formatState) constructOrigFormat(verb rune) (format string) {
|
||||
buf := bytes.NewBuffer(percentBytes)
|
||||
|
||||
for _, flag := range supportedFlags {
|
||||
if f.fs.Flag(int(flag)) {
|
||||
buf.WriteRune(flag)
|
||||
}
|
||||
}
|
||||
|
||||
if width, ok := f.fs.Width(); ok {
|
||||
buf.WriteString(strconv.Itoa(width))
|
||||
}
|
||||
|
||||
if precision, ok := f.fs.Precision(); ok {
|
||||
buf.Write(precisionBytes)
|
||||
buf.WriteString(strconv.Itoa(precision))
|
||||
}
|
||||
|
||||
buf.WriteRune(verb)
|
||||
|
||||
format = buf.String()
|
||||
return format
|
||||
}
|
||||
|
||||
// unpackValue returns values inside of non-nil interfaces when possible and
|
||||
// ensures that types for values which have been unpacked from an interface
|
||||
// are displayed when the show types flag is also set.
|
||||
// This is useful for data types like structs, arrays, slices, and maps which
|
||||
// can contain varying types packed inside an interface.
|
||||
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
|
||||
if v.Kind() == reflect.Interface {
|
||||
f.ignoreNextType = false
|
||||
if !v.IsNil() {
|
||||
v = v.Elem()
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// formatPtr handles formatting of pointers by indirecting them as necessary.
|
||||
func (f *formatState) formatPtr(v reflect.Value) {
|
||||
// Display nil if top level pointer is nil.
|
||||
showTypes := f.fs.Flag('#')
|
||||
if v.IsNil() && (!showTypes || f.ignoreNextType) {
|
||||
f.fs.Write(nilAngleBytes)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove pointers at or below the current depth from map used to detect
|
||||
// circular refs.
|
||||
for k, depth := range f.pointers {
|
||||
if depth >= f.depth {
|
||||
delete(f.pointers, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Keep list of all dereferenced pointers to possibly show later.
|
||||
pointerChain := make([]uintptr, 0)
|
||||
|
||||
// Figure out how many levels of indirection there are by derferencing
|
||||
// pointers and unpacking interfaces down the chain while detecting circular
|
||||
// references.
|
||||
nilFound := false
|
||||
cycleFound := false
|
||||
indirects := 0
|
||||
ve := v
|
||||
for ve.Kind() == reflect.Ptr {
|
||||
if ve.IsNil() {
|
||||
nilFound = true
|
||||
break
|
||||
}
|
||||
indirects++
|
||||
addr := ve.Pointer()
|
||||
pointerChain = append(pointerChain, addr)
|
||||
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
|
||||
cycleFound = true
|
||||
indirects--
|
||||
break
|
||||
}
|
||||
f.pointers[addr] = f.depth
|
||||
|
||||
ve = ve.Elem()
|
||||
if ve.Kind() == reflect.Interface {
|
||||
if ve.IsNil() {
|
||||
nilFound = true
|
||||
break
|
||||
}
|
||||
ve = ve.Elem()
|
||||
}
|
||||
}
|
||||
|
||||
// Display type or indirection level depending on flags.
|
||||
if showTypes && !f.ignoreNextType {
|
||||
f.fs.Write(openParenBytes)
|
||||
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
|
||||
f.fs.Write([]byte(ve.Type().String()))
|
||||
f.fs.Write(closeParenBytes)
|
||||
} else {
|
||||
if nilFound || cycleFound {
|
||||
indirects += strings.Count(ve.Type().String(), "*")
|
||||
}
|
||||
f.fs.Write(openAngleBytes)
|
||||
f.fs.Write([]byte(strings.Repeat("*", indirects)))
|
||||
f.fs.Write(closeAngleBytes)
|
||||
}
|
||||
|
||||
// Display pointer information depending on flags.
|
||||
if f.fs.Flag('+') && (len(pointerChain) > 0) {
|
||||
f.fs.Write(openParenBytes)
|
||||
for i, addr := range pointerChain {
|
||||
if i > 0 {
|
||||
f.fs.Write(pointerChainBytes)
|
||||
}
|
||||
printHexPtr(f.fs, addr)
|
||||
}
|
||||
f.fs.Write(closeParenBytes)
|
||||
}
|
||||
|
||||
// Display dereferenced value.
|
||||
switch {
|
||||
case nilFound == true:
|
||||
f.fs.Write(nilAngleBytes)
|
||||
|
||||
case cycleFound == true:
|
||||
f.fs.Write(circularShortBytes)
|
||||
|
||||
default:
|
||||
f.ignoreNextType = true
|
||||
f.format(ve)
|
||||
}
|
||||
}
|
||||
|
||||
// format is the main workhorse for providing the Formatter interface. It
|
||||
// uses the passed reflect value to figure out what kind of object we are
|
||||
// dealing with and formats it appropriately. It is a recursive function,
|
||||
// however circular data structures are detected and handled properly.
|
||||
func (f *formatState) format(v reflect.Value) {
|
||||
// Handle invalid reflect values immediately.
|
||||
kind := v.Kind()
|
||||
if kind == reflect.Invalid {
|
||||
f.fs.Write(invalidAngleBytes)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle pointers specially.
|
||||
if kind == reflect.Ptr {
|
||||
f.formatPtr(v)
|
||||
return
|
||||
}
|
||||
|
||||
// Print type information unless already handled elsewhere.
|
||||
if !f.ignoreNextType && f.fs.Flag('#') {
|
||||
f.fs.Write(openParenBytes)
|
||||
f.fs.Write([]byte(v.Type().String()))
|
||||
f.fs.Write(closeParenBytes)
|
||||
}
|
||||
f.ignoreNextType = false
|
||||
|
||||
// Call Stringer/error interfaces if they exist and the handle methods
|
||||
// flag is enabled.
|
||||
if !f.cs.DisableMethods {
|
||||
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
|
||||
if handled := handleMethods(f.cs, f.fs, v); handled {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case reflect.Invalid:
|
||||
// Do nothing. We should never get here since invalid has already
|
||||
// been handled above.
|
||||
|
||||
case reflect.Bool:
|
||||
printBool(f.fs, v.Bool())
|
||||
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
||||
printInt(f.fs, v.Int(), 10)
|
||||
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||
printUint(f.fs, v.Uint(), 10)
|
||||
|
||||
case reflect.Float32:
|
||||
printFloat(f.fs, v.Float(), 32)
|
||||
|
||||
case reflect.Float64:
|
||||
printFloat(f.fs, v.Float(), 64)
|
||||
|
||||
case reflect.Complex64:
|
||||
printComplex(f.fs, v.Complex(), 32)
|
||||
|
||||
case reflect.Complex128:
|
||||
printComplex(f.fs, v.Complex(), 64)
|
||||
|
||||
case reflect.Slice:
|
||||
if v.IsNil() {
|
||||
f.fs.Write(nilAngleBytes)
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case reflect.Array:
|
||||
f.fs.Write(openBracketBytes)
|
||||
f.depth++
|
||||
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||
f.fs.Write(maxShortBytes)
|
||||
} else {
|
||||
numEntries := v.Len()
|
||||
for i := 0; i < numEntries; i++ {
|
||||
if i > 0 {
|
||||
f.fs.Write(spaceBytes)
|
||||
}
|
||||
f.ignoreNextType = true
|
||||
f.format(f.unpackValue(v.Index(i)))
|
||||
}
|
||||
}
|
||||
f.depth--
|
||||
f.fs.Write(closeBracketBytes)
|
||||
|
||||
case reflect.String:
|
||||
f.fs.Write([]byte(v.String()))
|
||||
|
||||
case reflect.Interface:
|
||||
// The only time we should get here is for nil interfaces due to
|
||||
// unpackValue calls.
|
||||
if v.IsNil() {
|
||||
f.fs.Write(nilAngleBytes)
|
||||
}
|
||||
|
||||
case reflect.Ptr:
|
||||
// Do nothing. We should never get here since pointers have already
|
||||
// been handled above.
|
||||
|
||||
case reflect.Map:
|
||||
// nil maps should be indicated as different than empty maps
|
||||
if v.IsNil() {
|
||||
f.fs.Write(nilAngleBytes)
|
||||
break
|
||||
}
|
||||
|
||||
f.fs.Write(openMapBytes)
|
||||
f.depth++
|
||||
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||
f.fs.Write(maxShortBytes)
|
||||
} else {
|
||||
keys := v.MapKeys()
|
||||
if f.cs.SortKeys {
|
||||
sortValues(keys, f.cs)
|
||||
}
|
||||
for i, key := range keys {
|
||||
if i > 0 {
|
||||
f.fs.Write(spaceBytes)
|
||||
}
|
||||
f.ignoreNextType = true
|
||||
f.format(f.unpackValue(key))
|
||||
f.fs.Write(colonBytes)
|
||||
f.ignoreNextType = true
|
||||
f.format(f.unpackValue(v.MapIndex(key)))
|
||||
}
|
||||
}
|
||||
f.depth--
|
||||
f.fs.Write(closeMapBytes)
|
||||
|
||||
case reflect.Struct:
|
||||
numFields := v.NumField()
|
||||
f.fs.Write(openBraceBytes)
|
||||
f.depth++
|
||||
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
|
||||
f.fs.Write(maxShortBytes)
|
||||
} else {
|
||||
vt := v.Type()
|
||||
for i := 0; i < numFields; i++ {
|
||||
if i > 0 {
|
||||
f.fs.Write(spaceBytes)
|
||||
}
|
||||
vtf := vt.Field(i)
|
||||
if f.fs.Flag('+') || f.fs.Flag('#') {
|
||||
f.fs.Write([]byte(vtf.Name))
|
||||
f.fs.Write(colonBytes)
|
||||
}
|
||||
f.format(f.unpackValue(v.Field(i)))
|
||||
}
|
||||
}
|
||||
f.depth--
|
||||
f.fs.Write(closeBraceBytes)
|
||||
|
||||
case reflect.Uintptr:
|
||||
printHexPtr(f.fs, uintptr(v.Uint()))
|
||||
|
||||
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||
printHexPtr(f.fs, v.Pointer())
|
||||
|
||||
// There were not any other types at the time this code was written, but
|
||||
// fall back to letting the default fmt package handle it if any get added.
|
||||
default:
|
||||
format := f.buildDefaultFormat()
|
||||
if v.CanInterface() {
|
||||
fmt.Fprintf(f.fs, format, v.Interface())
|
||||
} else {
|
||||
fmt.Fprintf(f.fs, format, v.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
|
||||
// details.
|
||||
func (f *formatState) Format(fs fmt.State, verb rune) {
|
||||
f.fs = fs
|
||||
|
||||
// Use standard formatting for verbs that are not v.
|
||||
if verb != 'v' {
|
||||
format := f.constructOrigFormat(verb)
|
||||
fmt.Fprintf(fs, format, f.value)
|
||||
return
|
||||
}
|
||||
|
||||
if f.value == nil {
|
||||
if fs.Flag('#') {
|
||||
fs.Write(interfaceBytes)
|
||||
}
|
||||
fs.Write(nilAngleBytes)
|
||||
return
|
||||
}
|
||||
|
||||
f.format(reflect.ValueOf(f.value))
|
||||
}
|
||||
|
||||
// newFormatter is a helper function to consolidate the logic from the various
|
||||
// public methods which take varying config states.
|
||||
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
|
||||
fs := &formatState{value: v, cs: cs}
|
||||
fs.pointers = make(map[uintptr]int)
|
||||
return fs
|
||||
}
|
||||
|
||||
/*
|
||||
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
|
||||
interface. As a result, it integrates cleanly with standard fmt package
|
||||
printing functions. The formatter is useful for inline printing of smaller data
|
||||
types similar to the standard %v format specifier.
|
||||
|
||||
The custom formatter only responds to the %v (most compact), %+v (adds pointer
|
||||
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
|
||||
combinations. Any other verbs such as %x and %q will be sent to the the
|
||||
standard fmt package for formatting. In addition, the custom formatter ignores
|
||||
the width and precision arguments (however they will still work on the format
|
||||
specifiers not handled by the custom formatter).
|
||||
|
||||
Typically this function shouldn't be called directly. It is much easier to make
|
||||
use of the custom formatter by calling one of the convenience functions such as
|
||||
Printf, Println, or Fprintf.
|
||||
*/
|
||||
func NewFormatter(v interface{}) fmt.Formatter {
|
||||
return newFormatter(&Config, v)
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
package spew
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
|
||||
// passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the formatted string as a value that satisfies error. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Errorf(format string, a ...interface{}) (err error) {
|
||||
return fmt.Errorf(format, convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
|
||||
// passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprint(w, convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
|
||||
// passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprintf(w, format, convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
|
||||
// passed with a default Formatter interface returned by NewFormatter. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||
return fmt.Fprintln(w, convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Print is a wrapper for fmt.Print that treats each argument as if it were
|
||||
// passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Print(a ...interface{}) (n int, err error) {
|
||||
return fmt.Print(convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
|
||||
// passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Printf(format string, a ...interface{}) (n int, err error) {
|
||||
return fmt.Printf(format, convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Println is a wrapper for fmt.Println that treats each argument as if it were
|
||||
// passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the number of bytes written and any write error encountered. See
|
||||
// NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Println(a ...interface{}) (n int, err error) {
|
||||
return fmt.Println(convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
|
||||
// passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the resulting string. See NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Sprint(a ...interface{}) string {
|
||||
return fmt.Sprint(convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
|
||||
// passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the resulting string. See NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Sprintf(format string, a ...interface{}) string {
|
||||
return fmt.Sprintf(format, convertArgs(a)...)
|
||||
}
|
||||
|
||||
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
|
||||
// were passed with a default Formatter interface returned by NewFormatter. It
|
||||
// returns the resulting string. See NewFormatter for formatting details.
|
||||
//
|
||||
// This function is shorthand for the following syntax:
|
||||
//
|
||||
// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
|
||||
func Sprintln(a ...interface{}) string {
|
||||
return fmt.Sprintln(convertArgs(a)...)
|
||||
}
|
||||
|
||||
// convertArgs accepts a slice of arguments and returns a slice of the same
|
||||
// length with each argument converted to a default spew Formatter interface.
|
||||
func convertArgs(args []interface{}) (formatters []interface{}) {
|
||||
formatters = make([]interface{}, len(args))
|
||||
for index, arg := range args {
|
||||
formatters[index] = NewFormatter(arg)
|
||||
}
|
||||
return formatters
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.3.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
fast_finish: true
|
||||
install:
|
||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
||||
|
||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
||||
```
|
||||
|
||||
## English-specific functions
|
||||
|
||||
The following functions are in the `humanize/english` subpackage.
|
||||
|
||||
### Plurals
|
||||
|
||||
Simple English pluralization
|
||||
|
||||
```go
|
||||
english.PluralWord(1, "object", "") // object
|
||||
english.PluralWord(42, "object", "") // objects
|
||||
english.PluralWord(2, "bus", "") // buses
|
||||
english.PluralWord(99, "locus", "loci") // loci
|
||||
|
||||
english.Plural(1, "object", "") // 1 object
|
||||
english.Plural(42, "object", "") // 42 objects
|
||||
english.Plural(2, "bus", "") // 2 buses
|
||||
english.Plural(99, "locus", "loci") // 99 loci
|
||||
```
|
||||
|
||||
### Word series
|
||||
|
||||
Format comma-separated words lists with conjuctions:
|
||||
|
||||
```go
|
||||
english.WordSeries([]string{"foo"}, "and") // foo
|
||||
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar
|
||||
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz
|
||||
|
||||
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// order of magnitude (to a max order)
|
||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
if mag == maxmag && maxmag >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
||||
// total order of magnitude
|
||||
// (same as above, but with no upper limit)
|
||||
func oom(n, b *big.Int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bigIECExp = big.NewInt(1024)
|
||||
|
||||
// BigByte is one byte in bit.Ints
|
||||
BigByte = big.NewInt(1)
|
||||
// BigKiByte is 1,024 bytes in bit.Ints
|
||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
||||
)
|
||||
|
||||
var (
|
||||
bigSIExp = big.NewInt(1000)
|
||||
|
||||
// BigSIByte is one SI byte in big.Ints
|
||||
BigSIByte = big.NewInt(1)
|
||||
// BigKByte is 1,000 SI bytes in big.Ints
|
||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
||||
)
|
||||
|
||||
var bigBytesSizeTable = map[string]*big.Int{
|
||||
"b": BigByte,
|
||||
"kib": BigKiByte,
|
||||
"kb": BigKByte,
|
||||
"mib": BigMiByte,
|
||||
"mb": BigMByte,
|
||||
"gib": BigGiByte,
|
||||
"gb": BigGByte,
|
||||
"tib": BigTiByte,
|
||||
"tb": BigTByte,
|
||||
"pib": BigPiByte,
|
||||
"pb": BigPByte,
|
||||
"eib": BigEiByte,
|
||||
"eb": BigEByte,
|
||||
"zib": BigZiByte,
|
||||
"zb": BigZByte,
|
||||
"yib": BigYiByte,
|
||||
"yb": BigYByte,
|
||||
// Without suffix
|
||||
"": BigByte,
|
||||
"ki": BigKiByte,
|
||||
"k": BigKByte,
|
||||
"mi": BigMiByte,
|
||||
"m": BigMByte,
|
||||
"gi": BigGiByte,
|
||||
"g": BigGByte,
|
||||
"ti": BigTiByte,
|
||||
"t": BigTByte,
|
||||
"pi": BigPiByte,
|
||||
"p": BigPByte,
|
||||
"ei": BigEiByte,
|
||||
"e": BigEByte,
|
||||
"z": BigZByte,
|
||||
"zi": BigZiByte,
|
||||
"y": BigYByte,
|
||||
"yi": BigYiByte,
|
||||
}
|
||||
|
||||
var ten = big.NewInt(10)
|
||||
|
||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
||||
if s.Cmp(ten) < 0 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
c := (&big.Int{}).Set(s)
|
||||
val, mag := oomm(c, base, len(sizes)-1)
|
||||
suffix := sizes[mag]
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
|
||||
}
|
||||
|
||||
// BigBytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigBytes(82854982) -> 83 MB
|
||||
func BigBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
return humanateBigBytes(s, bigSIExp, sizes)
|
||||
}
|
||||
|
||||
// BigIBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigIBytes(82854982) -> 79 MiB
|
||||
func BigIBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
return humanateBigBytes(s, bigIECExp, sizes)
|
||||
}
|
||||
|
||||
// ParseBigBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See also: BigBytes, BigIBytes.
|
||||
//
|
||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
||||
func ParseBigBytes(s string) (*big.Int, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
val := &big.Rat{}
|
||||
_, err := fmt.Sscanf(num, "%f", val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
||||
mv := (&big.Rat{}).SetInt(m)
|
||||
val.Mul(val, mv)
|
||||
rv := &big.Int{}
|
||||
rv.Div(val.Num(), val.Denom())
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
// Bytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// Bytes(82854982) -> 83 MB
|
||||
func Bytes(s uint64) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
||||
|
||||
// IBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// IBytes(82854982) -> 79 MiB
|
||||
func IBytes(s uint64) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
return humanateBytes(s, 1024, sizes)
|
||||
}
|
||||
|
||||
// ParseBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See Also: Bytes, IBytes.
|
||||
//
|
||||
// ParseBytes("42 MB") -> 42000000, nil
|
||||
// ParseBytes("42 mib") -> 44040192, nil
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bytesSizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Comma produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Comma(834142) -> 834,142
|
||||
func Comma(v int64) string {
|
||||
sign := ""
|
||||
|
||||
// Min int64 can't be negated to a usable value, so it has to be special cased.
|
||||
if v == math.MinInt64 {
|
||||
return "-9,223,372,036,854,775,808"
|
||||
}
|
||||
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
parts := []string{"", "", "", "", "", "", ""}
|
||||
j := len(parts) - 1
|
||||
|
||||
for v > 999 {
|
||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
v = v / 1000
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(v))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
||||
// Commaf produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Commaf(834142.32) -> 834,142.32
|
||||
func Commaf(v float64) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// CommafWithDigits works like the Commaf but limits the resulting
|
||||
// string to the given number of decimal places.
|
||||
//
|
||||
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3
|
||||
func CommafWithDigits(f float64, decimals int) string {
|
||||
return stripTrailingDigits(Commaf(f), decimals)
|
||||
}
|
||||
|
||||
// BigComma produces a string form of the given big.Int in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigComma(b *big.Int) string {
|
||||
sign := ""
|
||||
if b.Sign() < 0 {
|
||||
sign = "-"
|
||||
b.Abs(b)
|
||||
}
|
||||
|
||||
athousand := big.NewInt(1000)
|
||||
c := (&big.Int{}).Set(b)
|
||||
_, m := oom(c, athousand)
|
||||
parts := make([]string, m+1)
|
||||
j := len(parts) - 1
|
||||
|
||||
mod := &big.Int{}
|
||||
for b.Cmp(athousand) >= 0 {
|
||||
b.DivMod(b, athousand, mod)
|
||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BigCommaf produces a string form of the given big.Float in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigCommaf(v *big.Float) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v.Sign() < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v.Abs(v)
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(v.Text('f', -1), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func stripTrailingZeros(s string) string {
|
||||
offset := len(s) - 1
|
||||
for offset > 0 {
|
||||
if s[offset] == '.' {
|
||||
offset--
|
||||
break
|
||||
}
|
||||
if s[offset] != '0' {
|
||||
break
|
||||
}
|
||||
offset--
|
||||
}
|
||||
return s[:offset+1]
|
||||
}
|
||||
|
||||
func stripTrailingDigits(s string, digits int) string {
|
||||
if i := strings.Index(s, "."); i >= 0 {
|
||||
if digits <= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
i++
|
||||
if i+digits >= len(s) {
|
||||
return s
|
||||
}
|
||||
return s[:i+digits]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Ftoa converts a float to a string with no trailing zeros.
|
||||
func Ftoa(num float64) string {
|
||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
||||
}
|
||||
|
||||
// FtoaWithDigits converts a float to a string but limits the resulting string
|
||||
// to the given number of decimal places, and no trailing zeros.
|
||||
func FtoaWithDigits(num float64, digits int) string {
|
||||
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits))
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
||||
|
||||
Durations can be turned into strings such as "3 days ago", numbers
|
||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
||||
"79 MiB" (whichever you prefer).
|
||||
*/
|
||||
package humanize
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
package humanize
|
||||
|
||||
/*
|
||||
Slightly adapted from the source to fit go-humanize.
|
||||
|
||||
Author: https://github.com/gorhill
|
||||
Source: https://gist.github.com/gorhill/5285193
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
renderFloatPrecisionMultipliers = [...]float64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
}
|
||||
|
||||
renderFloatPrecisionRounders = [...]float64{
|
||||
0.5,
|
||||
0.05,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.00005,
|
||||
0.000005,
|
||||
0.0000005,
|
||||
0.00000005,
|
||||
0.000000005,
|
||||
0.0000000005,
|
||||
}
|
||||
)
|
||||
|
||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||
// * thousands separator
|
||||
// * decimal separator
|
||||
// * decimal precision
|
||||
//
|
||||
// Usage: s := RenderFloat(format, n)
|
||||
// The format parameter tells how to render the number n.
|
||||
//
|
||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||
//
|
||||
// Examples of format strings, given n = 12345.6789:
|
||||
// "#,###.##" => "12,345.67"
|
||||
// "#,###." => "12,345"
|
||||
// "#,###" => "12345,678"
|
||||
// "#\u202F###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < -math.MaxFloat64 {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Ordinal gives you the input number in a rank/ordinal format.
|
||||
//
|
||||
// Ordinal(3) -> 3rd
|
||||
func Ordinal(x int) string {
|
||||
suffix := "th"
|
||||
switch x % 10 {
|
||||
case 1:
|
||||
if x%100 != 11 {
|
||||
suffix = "st"
|
||||
}
|
||||
case 2:
|
||||
if x%100 != 12 {
|
||||
suffix = "nd"
|
||||
}
|
||||
case 3:
|
||||
if x%100 != 13 {
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(x) + suffix
|
||||
}
|
||||