mirror of https://github.com/statping/statping
faster testing
parent
31148682ed
commit
5e3297d46b
|
@ -16,4 +16,5 @@ dev
|
|||
!dev/demo-script.sh
|
||||
!build/alpine-linux-amd64
|
||||
config.yml
|
||||
statup.db
|
||||
*.db
|
||||
tmp
|
||||
|
|
|
@ -5,7 +5,7 @@ stage
|
|||
parts
|
||||
core/rice-box.go
|
||||
config.yml
|
||||
statup.db
|
||||
*.db
|
||||
plugins/*.so
|
||||
data
|
||||
build
|
||||
|
@ -19,6 +19,7 @@ assets
|
|||
*.log
|
||||
.env
|
||||
logs
|
||||
tmp
|
||||
/dev/test/node_modules
|
||||
dev/test/cypress/videos
|
||||
dev/test/cypress/screenshots
|
||||
|
|
|
@ -7,7 +7,7 @@ cache:
|
|||
directories:
|
||||
- "~/.npm"
|
||||
- "~/.cache"
|
||||
- "/tmp/statping.db"
|
||||
- "$GOPATH/src/github.com/hunterlong/statping/tmp"
|
||||
- "$GOPATH/src/github.com/hunterlong/statping/vendor"
|
||||
sudo: required
|
||||
services:
|
||||
|
|
1
Makefile
1
Makefile
|
@ -256,7 +256,6 @@ clean:
|
|||
rm -f source/rice-box.go
|
||||
rm -rf **/*.db-journal
|
||||
rm -rf *.snap
|
||||
rm -rf /tmp/statping.db
|
||||
find . -name "*.out" -type f -delete
|
||||
find . -name "*.cpu" -type f -delete
|
||||
find . -name "*.mem" -type f -delete
|
||||
|
|
|
@ -97,6 +97,7 @@ func TestAssetsCommand(t *testing.T) {
|
|||
c.Run()
|
||||
t.Log(c.Stdout())
|
||||
t.Log("Directory for Assets: ", dir)
|
||||
time.Sleep(1 * time.Second)
|
||||
assert.FileExists(t, dir+"/assets/robots.txt")
|
||||
assert.FileExists(t, dir+"/assets/scss/base.scss")
|
||||
}
|
||||
|
|
|
@ -176,6 +176,7 @@ func EnvToConfig() (*DbConfig, error) {
|
|||
Password: adminPass,
|
||||
Error: nil,
|
||||
Location: utils.Directory,
|
||||
SqlFile: os.Getenv("SQL_FILE"),
|
||||
}
|
||||
return Configs, err
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ func init() {
|
|||
}
|
||||
|
||||
func TestNewCore(t *testing.T) {
|
||||
err := TmpRecords()
|
||||
err := TmpRecords("core.db")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, CoreApp)
|
||||
}
|
||||
|
|
|
@ -187,6 +187,9 @@ func (db *DbConfig) InsertCore() (*Core, error) {
|
|||
}
|
||||
|
||||
func findDbFile() string {
|
||||
if Configs.SqlFile != "" {
|
||||
return Configs.SqlFile
|
||||
}
|
||||
filename := types.SqliteFilename
|
||||
err := filepath.Walk(utils.Directory, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
|
@ -218,7 +221,7 @@ func (db *DbConfig) Connect(retry bool, location string) error {
|
|||
switch dbType {
|
||||
case "sqlite":
|
||||
sqlFilename := findDbFile()
|
||||
conn = location + "/" + sqlFilename
|
||||
conn = sqlFilename
|
||||
dbType = "sqlite3"
|
||||
case "mysql":
|
||||
host := fmt.Sprintf("%v:%v", Configs.DbHost, Configs.DbPort)
|
||||
|
|
|
@ -18,7 +18,6 @@ package notifier
|
|||
import (
|
||||
"fmt"
|
||||
"github.com/hunterlong/statping/types"
|
||||
"github.com/hunterlong/statping/utils"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -56,8 +55,7 @@ var core = &types.Core{
|
|||
}
|
||||
|
||||
func injectDatabase() {
|
||||
utils.DeleteFile(dir + types.SqliteFilename)
|
||||
db, _ = gorm.Open("sqlite3", dir+"/statup.db")
|
||||
db, _ = gorm.Open("sqlite3", dir+"/notifier.db")
|
||||
db.CreateTable(&Notification{})
|
||||
}
|
||||
|
||||
|
|
|
@ -495,18 +495,33 @@ func insertHitRecords(since time.Time, amount int64) {
|
|||
|
||||
}
|
||||
|
||||
// TmpRecordsDelete will delete the temporary SQLite database file
|
||||
func TmpRecordsDelete() error {
|
||||
return utils.DeleteFile("/tmp/" + types.SqliteFilename)
|
||||
}
|
||||
|
||||
// TmpRecords is used for testing Statping. It will create a SQLite database file
|
||||
// with sample data and store it in the /tmp folder to be used by the tests.
|
||||
func TmpRecords() error {
|
||||
var sqlFile = utils.Directory + "/" + types.SqliteFilename
|
||||
var tmpSqlFile = "/tmp/" + types.SqliteFilename
|
||||
func TmpRecords(dbFile string) error {
|
||||
var sqlFile = utils.Directory + "/" + dbFile
|
||||
utils.CreateDirectory(utils.Directory + "/tmp")
|
||||
var tmpSqlFile = utils.Directory + "/tmp/" + types.SqliteFilename
|
||||
SampleHits = 480
|
||||
|
||||
var err error
|
||||
CoreApp = NewCore()
|
||||
CoreApp.Name = "Tester"
|
||||
Configs = &DbConfig{
|
||||
DbConn: "sqlite",
|
||||
Project: "Tester",
|
||||
Location: utils.Directory,
|
||||
SqlFile: sqlFile,
|
||||
}
|
||||
log.Infoln("saving config.yml in: " + utils.Directory)
|
||||
if Configs, err = Configs.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("loading config.yml from: " + utils.Directory)
|
||||
if Configs, err = LoadConfigFile(utils.Directory); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("connecting to database")
|
||||
|
||||
exists := utils.FileExists(tmpSqlFile)
|
||||
if exists {
|
||||
log.Infoln(tmpSqlFile + " was found, copying the temp database to " + sqlFile)
|
||||
|
@ -517,10 +532,7 @@ func TmpRecords() error {
|
|||
return err
|
||||
}
|
||||
log.Infoln("loading config.yml from: " + utils.Directory)
|
||||
if _, err := LoadConfigFile(utils.Directory); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("connecting to database")
|
||||
|
||||
if err := Configs.Connect(false, utils.Directory); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -545,23 +557,6 @@ func TmpRecords() error {
|
|||
|
||||
log.Infoln(tmpSqlFile + " not found, creating a new database...")
|
||||
|
||||
var err error
|
||||
CoreApp = NewCore()
|
||||
CoreApp.Name = "Tester"
|
||||
Configs = &DbConfig{
|
||||
DbConn: "sqlite",
|
||||
Project: "Tester",
|
||||
Location: utils.Directory,
|
||||
}
|
||||
log.Infoln("saving config.yml in: " + utils.Directory)
|
||||
if Configs, err = Configs.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("loading config.yml from: " + utils.Directory)
|
||||
if Configs, err = LoadConfigFile(utils.Directory); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("connecting to database")
|
||||
if err := Configs.Connect(false, utils.Directory); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ func init() {
|
|||
}
|
||||
|
||||
func TestResetDatabase(t *testing.T) {
|
||||
err := core.TmpRecords()
|
||||
err := core.TmpRecords("handlers.db")
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, core.CoreApp)
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ func init() {
|
|||
|
||||
func injectDatabase() {
|
||||
utils.DeleteFile(dir + types.SqliteFilename)
|
||||
db, err := gorm.Open("sqlite3", dir+"/statping.db")
|
||||
db, err := gorm.Open("sqlite3", dir+"notifiers.db")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
383
statuper
383
statuper
|
@ -1,383 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
VERSION="0.1"
|
||||
VERBOSE=false
|
||||
SSLOPTION=false
|
||||
AWSREGION="us-west-2"
|
||||
US_W_1="ami-6d1ffd0e"
|
||||
US_W_2="ami-7be8a103"
|
||||
US_E_1="ami-b3be85cc"
|
||||
US_E_2="ami-cc7a40a9"
|
||||
AMI_IMAGE=$US_W_2
|
||||
AWS_CLI=$(which aws)
|
||||
DOCKER_CLI=$(which docker)
|
||||
DOCKER_IMG="hunterlong/statping"
|
||||
AWS_ECS="$AWS_CLI --output json"
|
||||
DOCKER_PORT=8080
|
||||
|
||||
function usage() {
|
||||
cat <<EOM
|
||||
##### Statping Installer #####
|
||||
A simple shell script that will help you install Statping on your local machine, AWS, or Docker.
|
||||
|
||||
Commands:
|
||||
install Install statping to your local system
|
||||
aws Create a new EC2 instance running Statping
|
||||
docker Start the latest Statping Docker image
|
||||
docker-compose Create Statping with a Postgres database
|
||||
version Return the latest version of Statping
|
||||
|
||||
Available Flags:
|
||||
-k | --aws-access-key AWS Access Key ID. May also be set as environment variable AWS_ACCESS_KEY_ID
|
||||
-s | --aws-secret-key AWS Secret Access Key. May also be set as environment variable AWS_SECRET_ACCESS_KEY
|
||||
-r | --region AWS Region Name. May also be set as environment variable AWS_DEFAULT_REGION
|
||||
-x | --verbose Verbose output
|
||||
|
||||
Visit the github repo at: https://github.com/hunterlong/statping
|
||||
EOM
|
||||
exit 3
|
||||
}
|
||||
|
||||
# Check requirements
|
||||
function require() {
|
||||
getOS
|
||||
command -v "$1" > /dev/null 2>&1 || {
|
||||
echo "Some of the required software is not installed:"
|
||||
APP="$1" >&2;
|
||||
echo " Required application: $APP"
|
||||
if [ $OS == "osx" ]; then
|
||||
echo " You can run 'brew install $APP'"
|
||||
elif [ $OS == "linux" ]; then
|
||||
echo " You can run 'apt install $APP'"
|
||||
fi
|
||||
exit 4;
|
||||
}
|
||||
}
|
||||
|
||||
# Get the latest release from github
|
||||
get_latest_release() {
|
||||
STATPING_VERSION=$(curl --silent "https://api.github.com/repos/$DOCKER_IMG/releases/latest" | jq -r .tag_name)
|
||||
}
|
||||
|
||||
# auto set AWS environment variables
|
||||
function setAWSPresets {
|
||||
if [ -z ${AWS_DEFAULT_REGION+x} ];
|
||||
then unset AWS_DEFAULT_REGION
|
||||
else
|
||||
AWS_ECS="$AWS_ECS --region $AWS_DEFAULT_REGION"
|
||||
fi
|
||||
if [ -z ${AWS_PROFILE+x} ];
|
||||
then unset AWS_PROFILE
|
||||
else
|
||||
AWS_ECS="$AWS_ECS --profile $AWS_PROFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# ask the user to inser their AWS region
|
||||
function awsAskRegion {
|
||||
if [ -z ${AWS_DEFAULT_REGION+x} ]; then
|
||||
read -p "Enter the AWS Region: " AWSREGION
|
||||
else
|
||||
AWSREGION=$AWS_DEFAULT_REGION
|
||||
fi
|
||||
}
|
||||
|
||||
# ask for the EC2 instance name
|
||||
function askEC2Name {
|
||||
read -p "Enter the Name for EC2 Instance: " SERVERNAME
|
||||
}
|
||||
|
||||
# ask user if they want to use SSL
|
||||
function askSSLOption {
|
||||
read -p "Do you want to install a SSL certificate? (y/N):" SSLOPTION
|
||||
}
|
||||
|
||||
# ask user for domain for SSL
|
||||
function askSSLDomain {
|
||||
read -p "Enter the Domain to attach the SSL certificate on: " SSLDOMAIN
|
||||
}
|
||||
|
||||
# ask user for email for Letencrypt
|
||||
function askSSLEmail {
|
||||
read -p "Enter the Email for Lets Encrypt: " SSLEMAIL
|
||||
}
|
||||
|
||||
# ask user for their EC2 Keypair for instance
|
||||
function askEC2KeyName {
|
||||
read -p "Enter the Keypair for EC2 Instance: " EC2KEYNAME
|
||||
}
|
||||
|
||||
# ask user to create a new AWS security group namne
|
||||
function askSecurityName {
|
||||
read -p "Enter a name for the new Security Group: " EC2SECGROUP
|
||||
}
|
||||
|
||||
# ask user if they want to install statping to the bin folder
|
||||
function askInstallGlobal {
|
||||
read -p "Do you want to move Statping to the bin folder? (y/N): " MOVEBIN
|
||||
}
|
||||
|
||||
# ask user if they want statping to start on boot
|
||||
function askInstallSystemCTL {
|
||||
read -p "Do you want to auto start Statping on boot? (y/N): " SYSTEMCTL
|
||||
}
|
||||
|
||||
# Task to create a new AWS security group
|
||||
function awsSecurityGroup {
|
||||
echo "Running task: Creating Security Group";
|
||||
GROUPID=`$AWS_ECS ec2 create-security-group --group-name "$EC2SECGROUP" --description "Statping HTTP Server on port 80 and 443" | jq -r .GroupId`
|
||||
echo "Created new security group: $GROUPID";
|
||||
awsAuthSecurityGroup
|
||||
}
|
||||
|
||||
# Task to open ports 80 and 443 for the security group
|
||||
function awsAuthSecurityGroup {
|
||||
$AWS_ECS ec2 authorize-security-group-ingress --group-id $GROUPID --protocol tcp --port 80 --cidr 0.0.0.0/0
|
||||
$AWS_ECS ec2 authorize-security-group-ingress --group-id $GROUPID --protocol tcp --port 443 --cidr 0.0.0.0/0
|
||||
echo "Authorize security group to be open on ports 80 and 443";
|
||||
}
|
||||
|
||||
function awsCreateEC2 {
|
||||
NEW_SRV=`$AWS_ECS ec2 run-instances --image-id $US_W_2 --count 1 --instance-type t2.nano --key-name $EC2KEYNAME --security-group-ids $GROUPID`
|
||||
INSTANCE_ID=$(echo "${NEW_SRV}" | jq -r .Instances[0].InstanceId)
|
||||
EC2_STATUS=$(echo "${NEW_SRV}" | jq -r .Instances[0].StateReason.Message)
|
||||
echo "New EC2 instance created: $INSTANCE_ID with status $EC2_STATUS";
|
||||
}
|
||||
|
||||
# task to created a new EC2 instance with statping image
|
||||
function ec2TaskComplete {
|
||||
echo "New EC2 instance is ready! $INSTANCE_ID with status $EC2_STATUS";
|
||||
echo "Instance ID: $INSTANCE_ID with status $EC2_STATUS";
|
||||
echo "Public DNS: $EC2_DNS";
|
||||
if [ $SSLOPTION == "y" ]; then
|
||||
echo "Now you have to add a CNAME DNS record on $SSLDOMAIN pointing to $EC2_DNS"
|
||||
fi
|
||||
}
|
||||
|
||||
# function to check the EC2 instance
|
||||
function checkEC2Instance {
|
||||
SRV_INFO=`$AWS_ECS ec2 describe-instances --instance-ids $INSTANCE_ID`
|
||||
EC2_STATUS=$(echo "${SRV_INFO}" | jq -r .Reservations[0].Instances[0].State.Name)
|
||||
EC2_DNS=$(echo "${SRV_INFO}" | jq -r .Reservations[0].Instances[0].PublicDnsName)
|
||||
EC2_STATUS=$(echo "${SRV_INFO}" | jq -r .Reservations[0].Instances[0].State.Name)
|
||||
if [ $EC2_STATUS == '"pending"' ]; then
|
||||
echo "EC2 instance is still being created: $INSTANCE_ID";
|
||||
sleep 2
|
||||
checkEC2Instance
|
||||
fi
|
||||
}
|
||||
|
||||
# function to create the Statping EC2 instance
|
||||
function awsTask {
|
||||
setAWSPresets
|
||||
askEC2Name
|
||||
awsAskRegion
|
||||
askSecurityName
|
||||
askEC2KeyName
|
||||
askSSLOption
|
||||
if [ $SSLOPTION == "y" ]; then
|
||||
askSSLDomain
|
||||
askSSLEmail
|
||||
fi
|
||||
awsSecurityGroup
|
||||
awsCreateEC2
|
||||
checkEC2Instance
|
||||
ec2TaskComplete
|
||||
}
|
||||
|
||||
# function to move the statping binary to the bin folder
|
||||
function moveToBin {
|
||||
mv statping /usr/local/bin/statping
|
||||
}
|
||||
|
||||
# function to install a systemctl service to the local system
|
||||
function installSystemCTL {
|
||||
FILE=statping.service
|
||||
cat > $FILE <<- EOM
|
||||
[Unit]
|
||||
Description=Statping Server
|
||||
After=network.target
|
||||
After=systemd-user-sessions.service
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
ExecStart=/usr/local/bin/statping
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
echo "Installing systemctl service file to: /etc/systemd/system/$FILE"
|
||||
mv $FILE /etc/systemd/system/$FILE
|
||||
systemctl daemon-reload
|
||||
systemctl enable statping.service
|
||||
systemctl start statping
|
||||
echo "Statping has been installed to SystemCTL and will start on boot"
|
||||
}
|
||||
|
||||
function downloadBin {
|
||||
getOS
|
||||
getArch
|
||||
get_latest_release
|
||||
GIT_DOWNLOAD="https://github.com/$DOCKER_IMG/releases/download/$STATPING_VERSION/statping-$OS-$ARCH.tar.gz"
|
||||
echo "Downloading Statping $STATPING_VERSION from $GIT_DOWNLOAD"
|
||||
curl -L --silent $GIT_DOWNLOAD | tar xz
|
||||
echo "Download complete"
|
||||
}
|
||||
|
||||
# install statping locally from github
|
||||
function localTask {
|
||||
downloadBin
|
||||
echo "Try Statping by running 'statping version'!"
|
||||
askInstallGlobal
|
||||
if [ $MOVEBIN == "y" ]; then
|
||||
moveToBin
|
||||
echo "Statping can now be ran anywhere with command 'statping'"
|
||||
echo "Install location: /usr/local/bin/statping"
|
||||
if [ $OS == "linux" ]; then
|
||||
askInstallSystemCTL
|
||||
if [ $SYSTEMCTL == "y" ]; then
|
||||
installSystemCTL
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function uninstallTask {
|
||||
rm -f /usr/local/bin/statping
|
||||
}
|
||||
|
||||
# start the Statping docker image
|
||||
function dockerTask {
|
||||
echo "Starting Statping Docker container on port $DOCKER_PORT"
|
||||
$DOCKER_CLI run -d -p $DOCKER_PORT:8080 $DOCKER_IMG
|
||||
}
|
||||
|
||||
# get 64x or 32 machine arch
|
||||
function getArch {
|
||||
MACHINE_TYPE=`uname -m`
|
||||
if [ ${MACHINE_TYPE} == 'x86_64' ]; then
|
||||
ARCH="x64"
|
||||
else
|
||||
ARCH="x32"
|
||||
fi
|
||||
}
|
||||
|
||||
# get the users operating system
|
||||
function getOS {
|
||||
OS="`uname`"
|
||||
case $OS in
|
||||
'Linux')
|
||||
OS='linux'
|
||||
alias ls='ls --color=auto'
|
||||
;;
|
||||
'FreeBSD')
|
||||
OS='freebsd'
|
||||
alias ls='ls -G'
|
||||
;;
|
||||
'WindowsNT')
|
||||
OS='windows'
|
||||
;;
|
||||
'Darwin')
|
||||
OS='osx'
|
||||
;;
|
||||
'SunOS')
|
||||
OS='solaris'
|
||||
;;
|
||||
'AIX') ;;
|
||||
*) ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function echoVersion {
|
||||
require jq
|
||||
get_latest_release
|
||||
echo "Statping Latest: $STATPING_VERSION"
|
||||
echo "Statpinger Tool: v$VERSION"
|
||||
}
|
||||
|
||||
# main CLI entrypoint
|
||||
if [ "$BASH_SOURCE" == "$0" ]; then
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -u
|
||||
set -e
|
||||
# If no args are provided, display usage information
|
||||
if [ $# == 0 ]; then usage; fi
|
||||
|
||||
COMMD=$1
|
||||
|
||||
# Loop through arguments, two at a time for key and value
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
key="$1"
|
||||
case $key in
|
||||
-k|--aws-access-key)
|
||||
AWS_ACCESS_KEY_ID="$2"
|
||||
shift # past argument
|
||||
;;
|
||||
-s|--aws-secret-key)
|
||||
AWS_SECRET_ACCESS_KEY="$2"
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--region)
|
||||
AWS_DEFAULT_REGION="$2"
|
||||
shift # past argument
|
||||
;;
|
||||
-p|--port)
|
||||
DOCKER_PORT="$2"
|
||||
shift # past argument
|
||||
;;
|
||||
-x|--verbose)
|
||||
VERBOSE=true
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
shift # past argument or value
|
||||
done
|
||||
|
||||
if [ $VERBOSE == true ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
case $COMMD in
|
||||
aws)
|
||||
require aws
|
||||
require jq
|
||||
awsTask
|
||||
exit 0
|
||||
;;
|
||||
docker)
|
||||
require docker
|
||||
dockerTask
|
||||
exit 0
|
||||
;;
|
||||
docker-compose)
|
||||
require docker-compose
|
||||
dockerComposeTask
|
||||
exit 0
|
||||
;;
|
||||
install)
|
||||
require jq
|
||||
require curl
|
||||
require tar
|
||||
localTask
|
||||
shift # past argument
|
||||
;;
|
||||
uninstall)
|
||||
uninstallTask
|
||||
shift # past argument
|
||||
;;
|
||||
version|v)
|
||||
echoVersion
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
shift # past argument or value
|
||||
fi
|
||||
exit 0
|
||||
|
||||
fi
|
|
@ -195,6 +195,13 @@ func DeleteDirectory(directory string) error {
|
|||
return os.RemoveAll(directory)
|
||||
}
|
||||
|
||||
// CreateDirectory will attempt to create a directory
|
||||
// CreateDirectory("assets")
|
||||
func CreateDirectory(directory string) error {
|
||||
Log.Infoln("creating directory: " + directory)
|
||||
return os.Mkdir(directory, os.ModePerm)
|
||||
}
|
||||
|
||||
// CopyFile will copy a file to a new directory
|
||||
// CopyFile("source.jpg", "/tmp/source.jpg")
|
||||
func CopyFile(src, dst string) error {
|
||||
|
|
Loading…
Reference in New Issue