Merge pull request #399 from crawlab-team/develop

Develop
This commit is contained in:
Marvin Zhang
2019-12-28 12:52:14 +08:00
committed by GitHub
39 changed files with 970 additions and 191 deletions

View File

@@ -1,6 +1,16 @@
# 0.4.2 (unknown)
# 0.4.2 (2019-12-26)
### Features / Enhancement
- **Disclaimer**. Added page for Disclaimer.
- **Call API to fetch version**. [#371](https://github.com/crawlab-team/crawlab/issues/371)
- **Configure to allow user registration**. [#346](https://github.com/crawlab-team/crawlab/issues/346)
- **Allow adding new users**.
- **More Advanced File Management**. Allow users to add / edit / rename / delete files. [#286](https://github.com/crawlab-team/crawlab/issues/286)
- **Optimized Spider Creation Process**. Allow users to create an empty customized spider before uploading the zip file.
- **Better Task Management**. Allow users to filter tasks by selecting through certian criterions. [#341](https://github.com/crawlab-team/crawlab/issues/341)
### Bug Fixes
- **Duplicated nodes**. [#391](https://github.com/crawlab-team/crawlab/issues/391)
- **"mongodb no reachable" error**. [#373](https://github.com/crawlab-team/crawlab/issues/373)
# 0.4.1 (2019-12-13)
### Features / Enhancement

View File

@@ -1,6 +1,6 @@
# 免责声明
本免责及隐私保护声明(以下简称“隐私声明”或“本声明”)适用于 Crawlab 开发组 (以下简称“开发组”)研发的系列软件(以下简称"Crawlab") 在您阅读本声明后若不同意此声明中的任何条款,或对本声明存在质疑,请立刻停止使用我们的软件。若您已经开始或正在使用 Crawlab则表示您已阅读并同意本声明的所有条款之约定。
本免责及隐私保护声明(以下简称“免责声明”或“本声明”)适用于 Crawlab 开发组 (以下简称“开发组”)研发的系列软件(以下简称"Crawlab") 在您阅读本声明后若不同意此声明中的任何条款,或对本声明存在质疑,请立刻停止使用我们的软件。若您已经开始或正在使用 Crawlab则表示您已阅读并同意本声明的所有条款之约定。
1. 总则:您通过安装 Crawlab 并使用 Crawlab 提供的服务与功能即表示您已经同意与开发组立本协议。开发组可随时执行全权决定更改“条款”。经修订的“条款”一经在 Github 免责声明页面上公布后,立即自动生效。
2. 本产品是基于Golang的分布式爬虫管理平台支持Python、NodeJS、Go、Java、PHP等多种编程语言以及多种爬虫框架。

View File

@@ -1,6 +1,6 @@
# Disclaimer
This Disclaimer and privacy protection statement (hereinafter referred to as "privacy statement" or "this statement") is applicable to the series of software (hereinafter referred to as "crawlab") developed by crawlab development group (hereinafter referred to as "development group") after you read this statement, if you do not agree with any terms in this statement or have doubts about this statement, please stop using our software immediately. If you have started or are using crawlab, you have read and agree to all terms of this statement.
This Disclaimer and privacy protection statement (hereinafter referred to as "disclaimer statement" or "this statement") is applicable to the series of software (hereinafter referred to as "crawlab") developed by crawlab development group (hereinafter referred to as "development group") after you read this statement, if you do not agree with any terms in this statement or have doubts about this statement, please stop using our software immediately. If you have started or are using crawlab, you have read and agree to all terms of this statement.
1. General: by installing crawlab and using the services and functions provided by crawlab, you have agreed to establish this agreement with the development team. The developer group may at any time change the terms at its sole discretion. The amended "terms" shall take effect automatically as soon as they are published on the GitHub disclaimer page.
2. This product is a distributed crawler management platform based on golang, supporting python, nodejs, go, Java, PHP and other programming languages as well as a variety of crawler frameworks.

View File

@@ -254,6 +254,9 @@ Crawlab使用起来很方便也很通用可以适用于几乎任何主流
<a href="https://github.com/hantmac">
<img src="https://avatars2.githubusercontent.com/u/7600925?s=460&v=4" height="80">
</a>
<a href="https://github.com/duanbin0414">
<img src="https://avatars3.githubusercontent.com/u/50389867?s=460&v=4" height="80">
</a>
## 社区 & 赞助

View File

@@ -219,6 +219,9 @@ Crawlab is easy to use, general enough to adapt spiders in any language and any
<a href="https://github.com/hantmac">
<img src="https://avatars2.githubusercontent.com/u/7600925?s=460&v=4" height="80">
</a>
<a href="https://github.com/duanbin0414">
<img src="https://avatars3.githubusercontent.com/u/50389867?s=460&v=4" height="80">
</a>
## Community & Sponsorship

View File

@@ -32,3 +32,6 @@ task:
workers: 4
other:
tmppath: "/tmp"
version: 0.4.2
setting:
allowRegister: "N"

View File

@@ -58,9 +58,9 @@ func (r *Redis) subscribe(ctx context.Context, consume ConsumeFunc, channel ...s
}
done <- nil
case <-tick.C:
//fmt.Printf("ping message \n")
if err := psc.Ping(""); err != nil {
done <- err
fmt.Printf("ping message error: %s \n", err)
//done <- err
}
case err := <-done:
close(done)

View File

@@ -114,9 +114,9 @@ func main() {
app.Use(middlewares.CORSMiddleware())
anonymousGroup := app.Group("/")
{
anonymousGroup.POST("/login", routes.Login) // 用户登录
anonymousGroup.PUT("/users", routes.PutUser) // 添加用户
anonymousGroup.POST("/login", routes.Login) // 用户登录
anonymousGroup.PUT("/users", routes.PutUser) // 添加用户
anonymousGroup.GET("/setting", routes.GetSetting) // 获取配置信息
}
authGroup := app.Group("/", middlewares.AuthorizationMiddleware())
{
@@ -129,18 +129,24 @@ func main() {
authGroup.GET("/nodes/:id/system", routes.GetSystemInfo) // 节点任务列表
authGroup.DELETE("/nodes/:id", routes.DeleteNode) // 删除节点
// 爬虫
authGroup.GET("/spiders", routes.GetSpiderList) // 爬虫列表
authGroup.GET("/spiders/:id", routes.GetSpider) // 爬虫详情
authGroup.POST("/spiders", routes.PutSpider) // 上传爬虫 TODO: 名称不对
authGroup.POST("/spiders/:id", routes.PostSpider) // 修改爬虫
authGroup.POST("/spiders/:id/publish", routes.PublishSpider) // 发布爬虫
authGroup.DELETE("/spiders/:id", routes.DeleteSpider) // 删除爬虫
authGroup.GET("/spiders/:id/tasks", routes.GetSpiderTasks) // 爬虫任务列表
authGroup.GET("/spiders/:id/file", routes.GetSpiderFile) // 爬虫文件读取
authGroup.POST("/spiders/:id/file", routes.PostSpiderFile) // 爬虫目录写入
authGroup.GET("/spiders/:id/dir", routes.GetSpiderDir) // 爬虫目录
authGroup.GET("/spiders/:id/stats", routes.GetSpiderStats) // 爬虫统计数据
authGroup.GET("/spider/types", routes.GetSpiderTypes) // 爬虫类型
authGroup.GET("/spiders", routes.GetSpiderList) // 爬虫列表
authGroup.GET("/spiders/:id", routes.GetSpider) // 爬虫详情
authGroup.PUT("/spiders", routes.PutSpider) // 添加爬虫
authGroup.POST("/spiders", routes.UploadSpider) // 上传爬虫
authGroup.POST("/spiders/:id", routes.PostSpider) // 修改爬虫
authGroup.POST("/spiders/:id/publish", routes.PublishSpider) // 发布爬虫
authGroup.POST("/spiders/:id/upload", routes.UploadSpiderFromId) // 上传爬虫ID
authGroup.DELETE("/spiders/:id", routes.DeleteSpider) // 删除爬虫
authGroup.GET("/spiders/:id/tasks", routes.GetSpiderTasks) // 爬虫任务列表
authGroup.GET("/spiders/:id/file", routes.GetSpiderFile) // 爬虫文件读取
authGroup.POST("/spiders/:id/file", routes.PostSpiderFile) // 爬虫文件更改
authGroup.PUT("/spiders/:id/file", routes.PutSpiderFile) // 爬虫文件创建
authGroup.PUT("/spiders/:id/dir", routes.PutSpiderDir) // 爬虫目录创建
authGroup.DELETE("/spiders/:id/file", routes.DeleteSpiderFile) // 爬虫文件删除
authGroup.POST("/spiders/:id/file/rename", routes.RenameSpiderFile) // 爬虫文件重命名
authGroup.GET("/spiders/:id/dir", routes.GetSpiderDir) // 爬虫目录
authGroup.GET("/spiders/:id/stats", routes.GetSpiderStats) // 爬虫统计数据
authGroup.GET("/spider/types", routes.GetSpiderTypes) // 爬虫类型
// 可配置爬虫
authGroup.GET("/config_spiders/:id/config", routes.GetConfigSpiderConfig) // 获取可配置爬虫配置
authGroup.POST("/config_spiders/:id/config", routes.PostConfigSpiderConfig) // 更改可配置爬虫配置
@@ -176,6 +182,8 @@ func main() {
authGroup.POST("/users/:id", routes.PostUser) // 更改用户
authGroup.DELETE("/users/:id", routes.DeleteUser) // 删除用户
authGroup.GET("/me", routes.GetMe) // 获取自己账户
// release版本
authGroup.GET("/version", routes.GetVersion) // 获取发布的版本
}
}

View File

@@ -63,7 +63,9 @@ func GetCurrentNode() (Node, error) {
// 如果获取失败
if err != nil {
// 如果为主节点,表示为第一次注册,插入节点信息
if IsMaster() {
// update: 增加具体错误过滤。防止加入多个master节点后续需要职责拆分
//只在master节点运行的时候才检测master节点的信息是否存在
if IsMaster() && err == mgo.ErrNotFound {
// 获取本机信息
ip, mac, key, err := GetNodeBaseInfo()
if err != nil {

View File

@@ -157,15 +157,15 @@ func GetSpiderByFileId(fileId bson.ObjectId) *Spider {
}
// 获取爬虫(根据名称)
func GetSpiderByName(name string) *Spider {
func GetSpiderByName(name string) Spider {
s, c := database.GetCol("spiders")
defer s.Close()
var result *Spider
var result Spider
if err := c.Find(bson.M{"name": name}).One(&result); err != nil {
log.Errorf("get spider error: %s, spider_name: %s", err.Error(), name)
//debug.PrintStack()
return nil
return result
}
return result
}

View File

@@ -40,7 +40,7 @@ func PutConfigSpider(c *gin.Context) {
}
// 判断爬虫是否存在
if spider := model.GetSpiderByName(spider.Name); spider != nil {
if spider := model.GetSpiderByName(spider.Name); spider.Name != "" {
HandleErrorF(http.StatusBadRequest, c, fmt.Sprintf("spider for '%s' already exists", spider.Name))
return
}

33
backend/routes/setting.go Normal file
View File

@@ -0,0 +1,33 @@
package routes
import (
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"net/http"
)
type SettingBody struct {
AllowRegister string `json:"allow_register"`
}
func GetVersion(c *gin.Context) {
version := viper.GetString("version")
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
Data: version,
})
}
func GetSetting(c *gin.Context) {
allowRegister := viper.GetString("setting.allowRegister")
body := SettingBody{AllowRegister: allowRegister}
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
Data: body,
})
}

View File

@@ -7,6 +7,7 @@ import (
"crawlab/model"
"crawlab/services"
"crawlab/utils"
"fmt"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/globalsign/mgo"
@@ -17,6 +18,7 @@ import (
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"runtime/debug"
"strconv"
@@ -117,6 +119,64 @@ func PublishSpider(c *gin.Context) {
}
func PutSpider(c *gin.Context) {
var spider model.Spider
if err := c.ShouldBindJSON(&spider); err != nil {
HandleError(http.StatusBadRequest, c, err)
return
}
// 爬虫名称不能为空
if spider.Name == "" {
HandleErrorF(http.StatusBadRequest, c, "spider name should not be empty")
return
}
// 判断爬虫是否存在
if spider := model.GetSpiderByName(spider.Name); spider.Name != "" {
HandleErrorF(http.StatusBadRequest, c, fmt.Sprintf("spider for '%s' already exists", spider.Name))
return
}
// 设置爬虫类别
spider.Type = constants.Customized
// 将FileId置空
spider.FileId = bson.ObjectIdHex(constants.ObjectIdNull)
// 创建爬虫目录
spiderDir := filepath.Join(viper.GetString("spider.path"), spider.Name)
if utils.Exists(spiderDir) {
if err := os.RemoveAll(spiderDir); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
}
if err := os.MkdirAll(spiderDir, 0777); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
spider.Src = spiderDir
// 添加爬虫到数据库
if err := spider.Add(); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 同步到GridFS
if err := services.UploadSpiderToGridFsFromMaster(spider); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
Data: spider,
})
}
func UploadSpider(c *gin.Context) {
// 从body中获取文件
uploadFile, err := c.FormFile("file")
if err != nil {
@@ -178,7 +238,7 @@ func PutSpider(c *gin.Context) {
// 判断爬虫是否存在
spiderName := strings.Replace(targetFilename, ".zip", "", 1)
spider := model.GetSpiderByName(spiderName)
if spider == nil {
if spider.Name == "" {
// 保存爬虫信息
srcPath := viper.GetString("spider.path")
spider := model.Spider{
@@ -195,6 +255,96 @@ func PutSpider(c *gin.Context) {
_ = spider.Save()
}
// 发起同步
services.PublishAllSpiders()
// 获取爬虫
spider = model.GetSpiderByName(spiderName)
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
Data: spider,
})
}
func UploadSpiderFromId(c *gin.Context) {
// TODO: 与 UploadSpider 部分逻辑重复,需要优化代码
// 爬虫ID
spiderId := c.Param("id")
// 获取爬虫
spider, err := model.GetSpider(bson.ObjectIdHex(spiderId))
if err != nil {
if err == mgo.ErrNotFound {
HandleErrorF(http.StatusNotFound, c, "cannot find spider")
} else {
HandleError(http.StatusInternalServerError, c, err)
}
return
}
// 从body中获取文件
uploadFile, err := c.FormFile("file")
if err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 如果不为zip文件返回错误
if !strings.HasSuffix(uploadFile.Filename, ".zip") {
debug.PrintStack()
HandleError(http.StatusBadRequest, c, errors.New("Not a valid zip file"))
return
}
// 以防tmp目录不存在
tmpPath := viper.GetString("other.tmppath")
if !utils.Exists(tmpPath) {
if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
log.Error("mkdir other.tmppath dir error:" + err.Error())
debug.PrintStack()
HandleError(http.StatusBadRequest, c, errors.New("Mkdir other.tmppath dir error"))
return
}
}
// 保存到本地临时文件
randomId := uuid.NewV4()
tmpFilePath := filepath.Join(tmpPath, randomId.String()+".zip")
if err := c.SaveUploadedFile(uploadFile, tmpFilePath); err != nil {
log.Error("save upload file error: " + err.Error())
debug.PrintStack()
HandleError(http.StatusInternalServerError, c, err)
return
}
// 获取 GridFS 实例
s, gf := database.GetGridFs("files")
defer s.Close()
// 判断文件是否已经存在
var gfFile model.GridFs
if err := gf.Find(bson.M{"filename": uploadFile.Filename}).One(&gfFile); err == nil {
// 已经存在文件,则删除
_ = gf.RemoveId(gfFile.Id)
}
// 上传到GridFs
fid, err := services.UploadToGridFs(uploadFile.Filename, tmpFilePath)
if err != nil {
log.Errorf("upload to grid fs error: %s", err.Error())
debug.PrintStack()
return
}
// 更新file_id
spider.FileId = fid
_ = spider.Save()
// 发起同步
services.PublishSpider(spider)
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
@@ -283,6 +433,14 @@ func GetSpiderDir(c *gin.Context) {
})
}
// 爬虫文件管理
type SpiderFileReqBody struct {
Path string `json:"path"`
Content string `json:"content"`
NewPath string `json:"new_path"`
}
func GetSpiderFile(c *gin.Context) {
// 爬虫ID
id := c.Param("id")
@@ -311,11 +469,6 @@ func GetSpiderFile(c *gin.Context) {
})
}
type SpiderFileReqBody struct {
Path string `json:"path"`
Content string `json:"content"`
}
func PostSpiderFile(c *gin.Context) {
// 爬虫ID
id := c.Param("id")
@@ -340,6 +493,12 @@ func PostSpiderFile(c *gin.Context) {
return
}
// 同步到GridFS
if err := services.UploadSpiderToGridFsFromMaster(spider); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 返回结果
c.JSON(http.StatusOK, Response{
Status: "ok",
@@ -347,6 +506,161 @@ func PostSpiderFile(c *gin.Context) {
})
}
func PutSpiderFile(c *gin.Context) {
spiderId := c.Param("id")
var reqBody SpiderFileReqBody
if err := c.ShouldBindJSON(&reqBody); err != nil {
HandleError(http.StatusBadRequest, c, err)
return
}
spider, err := model.GetSpider(bson.ObjectIdHex(spiderId))
if err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 文件路径
filePath := path.Join(spider.Src, reqBody.Path)
// 如果文件已存在,则报错
if utils.Exists(filePath) {
HandleErrorF(http.StatusInternalServerError, c, fmt.Sprintf(`%s already exists`, filePath))
return
}
// 写入文件
if err := ioutil.WriteFile(filePath, []byte(reqBody.Content), 0777); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 同步到GridFS
if err := services.UploadSpiderToGridFsFromMaster(spider); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
})
}
func PutSpiderDir(c *gin.Context) {
spiderId := c.Param("id")
var reqBody SpiderFileReqBody
if err := c.ShouldBindJSON(&reqBody); err != nil {
HandleError(http.StatusBadRequest, c, err)
return
}
spider, err := model.GetSpider(bson.ObjectIdHex(spiderId))
if err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 文件路径
filePath := path.Join(spider.Src, reqBody.Path)
// 如果文件已存在,则报错
if utils.Exists(filePath) {
HandleErrorF(http.StatusInternalServerError, c, fmt.Sprintf(`%s already exists`, filePath))
return
}
// 创建文件夹
if err := os.MkdirAll(filePath, 0777); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 同步到GridFS
if err := services.UploadSpiderToGridFsFromMaster(spider); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
})
}
func DeleteSpiderFile(c *gin.Context) {
spiderId := c.Param("id")
var reqBody SpiderFileReqBody
if err := c.ShouldBindJSON(&reqBody); err != nil {
HandleError(http.StatusBadRequest, c, err)
return
}
spider, err := model.GetSpider(bson.ObjectIdHex(spiderId))
if err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
filePath := path.Join(spider.Src, reqBody.Path)
if err := os.RemoveAll(filePath); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 同步到GridFS
if err := services.UploadSpiderToGridFsFromMaster(spider); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
})
}
func RenameSpiderFile(c *gin.Context) {
spiderId := c.Param("id")
var reqBody SpiderFileReqBody
if err := c.ShouldBindJSON(&reqBody); err != nil {
HandleError(http.StatusBadRequest, c, err)
}
spider, err := model.GetSpider(bson.ObjectIdHex(spiderId))
if err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 原文件路径
filePath := path.Join(spider.Src, reqBody.Path)
newFilePath := path.Join(spider.Src, reqBody.NewPath)
// 如果新文件已存在,则报错
if utils.Exists(newFilePath) {
HandleErrorF(http.StatusInternalServerError, c, fmt.Sprintf(`%s already exists`, newFilePath))
return
}
// 重命名
if err := os.Rename(filePath, newFilePath); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
// 删除原文件
if err := os.RemoveAll(filePath); err != nil {
HandleError(http.StatusInternalServerError, c, err)
}
// 同步到GridFS
if err := services.UploadSpiderToGridFsFromMaster(spider); err != nil {
HandleError(http.StatusInternalServerError, c, err)
return
}
c.JSON(http.StatusOK, Response{
Status: "ok",
Message: "success",
})
}
// 爬虫类型
func GetSpiderTypes(c *gin.Context) {
types, err := model.GetSpiderTypes()

View File

@@ -21,6 +21,7 @@ type UserListRequestData struct {
type UserRequestData struct {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
}
func GetUser(c *gin.Context) {
@@ -88,11 +89,16 @@ func PutUser(c *gin.Context) {
return
}
// 默认为正常用户
if reqData.Role == "" {
reqData.Role = constants.RoleNormal
}
// 添加用户
user := model.User{
Username: strings.ToLower(reqData.Username),
Password: utils.EncryptPassword(reqData.Password),
Role: constants.RoleNormal,
Role: reqData.Role,
}
if err := user.Add(); err != nil {
HandleError(http.StatusInternalServerError, c, err)

View File

@@ -167,27 +167,34 @@ func UpdateNodeData() {
debug.PrintStack()
return
}
// 构造节点数据
data := Data{
Key: key,
Mac: mac,
Ip: ip,
Master: model.IsMaster(),
UpdateTs: time.Now(),
UpdateTsUnix: time.Now().Unix(),
//先获取所有Redis的nodekey
list, _ := database.RedisClient.HKeys("nodes")
if i := utils.Contains(list, key); i == false {
// 构造节点数据
data := Data{
Key: key,
Mac: mac,
Ip: ip,
Master: model.IsMaster(),
UpdateTs: time.Now(),
UpdateTsUnix: time.Now().Unix(),
}
// 注册节点到Redis
dataBytes, err := json.Marshal(&data)
if err != nil {
log.Errorf(err.Error())
debug.PrintStack()
return
}
if err := database.RedisClient.HSet("nodes", key, utils.BytesToString(dataBytes)); err != nil {
log.Errorf(err.Error())
return
}
}
// 注册节点到Redis
dataBytes, err := json.Marshal(&data)
if err != nil {
log.Errorf(err.Error())
debug.PrintStack()
return
}
if err := database.RedisClient.HSet("nodes", key, utils.BytesToString(dataBytes)); err != nil {
log.Errorf(err.Error())
return
}
}
func MasterNodeCallback(message redis.Message) (err error) {

View File

@@ -12,6 +12,7 @@ import (
"github.com/apex/log"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
uuid "github.com/satori/go.uuid"
"github.com/spf13/viper"
"os"
"path/filepath"
@@ -30,6 +31,48 @@ type SpiderUploadMessage struct {
SpiderId string
}
// 从主节点上传爬虫到GridFS
func UploadSpiderToGridFsFromMaster(spider model.Spider) error {
// 爬虫所在目录
spiderDir := spider.Src
// 打包为 zip 文件
files, err := utils.GetFilesFromDir(spiderDir)
if err != nil {
return err
}
randomId := uuid.NewV4()
tmpFilePath := filepath.Join(viper.GetString("other.tmppath"), spider.Name+"."+randomId.String()+".zip")
spiderZipFileName := spider.Name + ".zip"
if err := utils.Compress(files, tmpFilePath); err != nil {
return err
}
// 获取 GridFS 实例
s, gf := database.GetGridFs("files")
defer s.Close()
// 判断文件是否已经存在
var gfFile model.GridFs
if err := gf.Find(bson.M{"filename": spiderZipFileName}).One(&gfFile); err == nil {
// 已经存在文件,则删除
_ = gf.RemoveId(gfFile.Id)
}
// 上传到GridFs
fid, err := UploadToGridFs(spiderZipFileName, tmpFilePath)
if err != nil {
log.Errorf("upload to grid fs error: %s", err.Error())
return err
}
// 保存爬虫 FileId
spider.FileId = fid
_ = spider.Save()
return nil
}
// 上传zip文件到GridFS
func UploadToGridFs(fileName string, filePath string) (fid bson.ObjectId, err error) {
fid = ""

View File

@@ -6,6 +6,7 @@ import (
"github.com/apex/log"
"github.com/gomodule/redigo/redis"
"io"
"reflect"
"runtime/debug"
"unsafe"
)
@@ -40,3 +41,20 @@ func Close(c io.Closer) {
//log.WithError(err).Error("关闭资源文件失败。")
}
}
func Contains(array interface{}, val interface{}) (fla bool) {
fla = false
switch reflect.TypeOf(array).Kind() {
case reflect.Slice:
{
s := reflect.ValueOf(array)
for i := 0; i < s.Len(); i++ {
if reflect.DeepEqual(val, s.Index(i).Interface()) {
fla = true
return
}
}
}
}
return
}

View File

@@ -6,25 +6,25 @@ services:
environment:
CRAWLAB_API_ADDRESS: "http://localhost:8000" # backend API address 后端 API 地址,设置为 http://<宿主机IP>:<端口>,端口为映射出来的端口
CRAWLAB_SERVER_MASTER: "Y" # whether to be master node 是否为主节点,主节点为 Y工作节点为 N
CRAWLAB_MONGO_HOST: "mongo1" # MongoDB host address MongoDB 的地址,在 docker compose 网络中,直接引用服务名称
CRAWLAB_MONGO_HOST: "mongo" # MongoDB host address MongoDB 的地址,在 docker compose 网络中,直接引用服务名称
CRAWLAB_REDIS_ADDRESS: "redis" # Redis host address Redis 的地址,在 docker compose 网络中,直接引用服务名称
ports:
- "8080:8080" # frontend port mapping 前端端口映射
- "8000:8000" # backend port mapping 后端端口映射
depends_on:
- mongo1
- mongo
- redis
worker:
image: tikazyq/crawlab:latest
container_name: worker
environment:
CRAWLAB_SERVER_MASTER: "N"
CRAWLAB_MONGO_HOST: "mongo1"
CRAWLAB_MONGO_HOST: "mongo"
CRAWLAB_REDIS_ADDRESS: "redis"
depends_on:
- mongo1
- mongo
- redis
mongo1:
mongo:
image: mongo:latest
restart: always
# volumes:

View File

@@ -1,6 +1,6 @@
{
"name": "crawlab",
"version": "0.4.1",
"version": "0.4.2",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --ip=0.0.0.0 --mode=development",

View File

@@ -6,6 +6,9 @@
</template>
<script>
import {
mapState
} from 'vuex'
import DialogView from './components/Common/DialogView'
export default {
@@ -19,11 +22,15 @@ export default {
DialogView
},
computed: {
...mapState('setting', ['setting']),
useStats () {
return localStorage.getItem('useStats')
}
},
methods: {},
created () {
this.$store.dispatch('setting/getSetting')
},
mounted () {
window.setUseStats = (value) => {
localStorage.setItem('useStats', value)

View File

@@ -29,10 +29,14 @@
<el-form-item :label="$t('Parameters')" prop="param" inline-message>
<el-input v-model="form.param" :placeholder="$t('Parameters')"></el-input>
</el-form-item>
<el-form-item class="disclaimer-wrapper">
<el-checkbox v-model="isAllowDisclaimer"/>
<span style="margin-left: 5px">我已阅读并同意 <a href="javascript:" @click="onClickDisclaimer">免责声明</a> 所有内容</span>
</el-form-item>
</el-form>
<template slot="footer">
<el-button type="plain" size="small" @click="$emit('close')">{{$t('Cancel')}}</el-button>
<el-button type="primary" size="small" @click="onConfirm">{{$t('Confirm')}}</el-button>
<el-button type="primary" size="small" @click="onConfirm" :disabled="!isAllowDisclaimer">{{$t('Confirm')}}</el-button>
</template>
</el-dialog>
</template>
@@ -59,7 +63,8 @@ export default {
nodeIds: undefined,
param: '',
nodeList: []
}
},
isAllowDisclaimer: true
}
},
methods: {
@@ -82,6 +87,9 @@ export default {
this.$emit('close')
this.$st.sendEv('爬虫确认', '确认运行', this.form.runType)
})
},
onClickDisclaimer () {
this.$router.push('/disclaimer')
}
},
created () {
@@ -105,4 +113,8 @@ export default {
.crawl-confirm-dialog >>> .el-form .el-form-item {
margin-bottom: 20px;
}
.crawl-confirm-dialog >>> .disclaimer-wrapper a {
color: #409eff;
}
</style>

View File

@@ -7,15 +7,57 @@
<font-awesome-icon :icon="['fa', 'arrow-left']"/>
{{$t('Back')}}
</el-button>
<el-button v-if="showFile" type="primary" size="small" style="margin-right: 10px;" @click="onFileSave">
<el-popover v-model="isShowDelete">
<el-button size="small" type="default" @click="() => this.isShowDelete = false">
{{$t('Cancel')}}
</el-button>
<el-button size="small" type="danger" @click="onFileDelete">
{{$t('Confirm')}}
</el-button>
<template slot="reference">
<el-button v-if="currentPath !== ''" type="danger" size="small" style="margin-right: 10px;"
@click="() => this.isShowDelete = true">
<font-awesome-icon :icon="['fa', 'trash']"/>
{{$t('Remove')}}
</el-button>
</template>
</el-popover>
<el-popover v-model="isShowRename">
<el-input v-model="name" :placeholder="$t('Name')" style="margin-bottom: 10px"/>
<div style="text-align: right">
<el-button size="small" type="warning" @click="onRenameFile">
{{$t('Confirm')}}
</el-button>
</div>
<template slot="reference">
<el-button v-if="showFile" type="warning" size="small" style="margin-right: 10px;"
@click="onOpenRename">
<font-awesome-icon :icon="['fa', 'redo']"/>
{{$t('Rename')}}
</el-button>
</template>
</el-popover>
<el-button v-if="showFile" type="success" size="small" style="margin-right: 10px;" @click="onFileSave">
<font-awesome-icon :icon="['fa', 'save']"/>
{{$t('Save')}}
</el-button>
<!--TODO: new files/directories-->
<el-button v-if="false" type="primary" size="small" style="margin-right: 10px;">
<font-awesome-icon :icon="['fa', 'file-alt']"/>
{{$t('New File')}}
</el-button>
<el-popover v-if="!showFile" v-model="isShowAdd" @hide="onHideAdd">
<el-input v-model="name" :placeholder="$t('Name')"/>
<div class="add-type-list">
<el-button size="small" type="success" icon="el-icon-document-add" @click="onAddFile">
{{$t('File')}}
</el-button>
<el-button size="small" type="primary" icon="el-icon-folder-add" @click="onAddDir">
{{$t('Directory')}}
</el-button>
</div>
<template slot="reference">
<el-button type="primary" size="small" style="margin-right: 10px;">
<font-awesome-icon :icon="['fa', 'plus']"/>
{{$t('Create')}}
</el-button>
</template>
</el-popover>
</div>
<!--./back-->
@@ -70,7 +112,11 @@ export default {
return {
code: 'var hello = \'world\'',
isEdit: false,
showFile: false
showFile: false,
name: '',
isShowAdd: false,
isShowDelete: false,
isShowRename: false
}
},
computed: {
@@ -101,6 +147,7 @@ export default {
this.$store.commit('file/SET_CURRENT_PATH', item.path)
this.$store.dispatch('file/getFileContent', { path: item.path })
}
this.$st.sendEv('爬虫详情', '文件', '点击')
},
onBack () {
const sep = '/'
@@ -109,16 +156,71 @@ export default {
const path = arr.join(sep)
this.$store.commit('file/SET_CURRENT_PATH', path)
this.$store.dispatch('file/getFileList', { path: this.currentPath })
this.$st.sendEv('爬虫详情', '文件', '回退')
},
onFileSave () {
this.$store.dispatch('file/saveFileContent', { path: this.currentPath })
.then(() => {
this.$message.success(this.$t('Saved file successfully'))
})
async onFileSave () {
await this.$store.dispatch('file/saveFileContent', { path: this.currentPath })
this.$message.success(this.$t('Saved file successfully'))
this.$st.sendEv('爬虫详情', '文件', '保存')
},
onBackFile () {
this.showFile = false
this.onBack()
},
onHideAdd () {
this.name = ''
},
async onAddFile () {
if (!this.name) {
this.$message.error(this.$t('Name cannot be empty'))
return
}
const path = this.currentPath + '/' + this.name
await this.$store.dispatch('file/addFile', { path })
await this.$store.dispatch('file/getFileList', { path: this.currentPath })
this.isShowAdd = false
this.showFile = true
this.$store.commit('file/SET_FILE_CONTENT', '')
this.$store.commit('file/SET_CURRENT_PATH', path)
await this.$store.dispatch('file/getFileContent', { path })
this.$st.sendEv('爬虫详情', '文件', '添加')
},
async onAddDir () {
if (!this.name) {
this.$message.error(this.$t('Name cannot be empty'))
return
}
await this.$store.dispatch('file/addDir', { path: this.currentPath + '/' + this.name })
await this.$store.dispatch('file/getFileList', { path: this.currentPath })
this.isShowAdd = false
this.$st.sendEv('爬虫详情', '文件', '添加')
},
async onFileDelete () {
await this.$store.dispatch('file/deleteFile', { path: this.currentPath })
this.$message.success(this.$t('Deleted successfully'))
this.isShowDelete = false
this.onBackFile()
this.$st.sendEv('爬虫详情', '文件', '删除')
},
onOpenRename () {
this.isShowRename = true
const arr = this.currentPath.split('/')
this.name = arr[arr.length - 1]
},
async onRenameFile () {
let newPath
if (this.currentPath.split('/').length === 1) {
newPath = this.name
} else {
const arr = this.currentPath.split('/')
newPath = arr[0] + '/' + this.name
}
await this.$store.dispatch('file/renameFile', { path: this.currentPath, newPath })
this.$store.commit('file/SET_CURRENT_PATH', newPath)
this.$message.success(this.$t('Renamed successfully'))
this.isShowRename = false
this.$st.sendEv('爬虫详情', '文件', '重命名')
}
},
created () {
@@ -236,4 +338,14 @@ export default {
font-size: 14px;
color: rgba(3, 47, 98, 1);
}
.add-type-list {
text-align: right;
margin-top: 10px;
}
.add-type {
cursor: pointer;
font-weight: bolder;
}
</style>

View File

@@ -21,7 +21,8 @@
<el-form-item :label="$t('Source Folder')">
<el-input v-model="spiderForm.src" :placeholder="$t('Source Folder')" disabled></el-input>
</el-form-item>
<el-form-item v-if="spiderForm.type === 'customized'" :label="$t('Execute Command')" prop="cmd" required :inline-message="true">
<el-form-item v-if="spiderForm.type === 'customized'" :label="$t('Execute Command')" prop="cmd" required
:inline-message="true">
<el-input v-model="spiderForm.cmd" :placeholder="$t('Execute Command')"
:disabled="isView"></el-input>
</el-form-item>
@@ -45,20 +46,41 @@
</el-select>
</el-form-item>
<el-form-item :label="$t('Remark')">
<el-input v-model="spiderForm.remark"/>
<el-input type="textarea" v-model="spiderForm.remark" :placeholder="$t('Remark')"/>
</el-form-item>
</el-form>
</el-row>
<el-row class="button-container" v-if="!isView">
<el-button size="small" v-if="isShowRun" type="danger" @click="onCrawl">{{$t('Run')}}</el-button>
<el-button size="small" type="success" @click="onSave">{{$t('Save')}}</el-button>
<el-upload
v-if="spiderForm.type === 'customized'"
:action="$request.baseUrl + `/spiders/${spiderForm._id}/upload`"
:headers="{Authorization:token}"
:on-progress="() => this.uploadLoading = true"
:on-error="onUploadError"
:on-success="onUploadSuccess"
:file-list="fileList"
style="display:inline-block;margin-right:10px"
>
<el-button size="normal" type="primary" icon="el-icon-upload" v-loading="uploadLoading">
{{$t('Upload')}}
</el-button>
</el-upload>
<el-button size="normal" v-if="isShowRun" type="danger" @click="onCrawl"
icon="el-icon-video-play">
{{$t('Run')}}
</el-button>
<el-button size="normal" type="success" @click="onSave"
icon="el-icon-check">
{{$t('Save')}}
</el-button>
</el-row>
</div>
</template>
<script>
import {
mapState
mapState,
mapGetters
} from 'vuex'
import CrawlConfirmDialog from '../Common/CrawlConfirmDialog'
@@ -88,6 +110,8 @@ export default {
callback()
}
return {
uploadLoading: false,
fileList: [],
crawlConfirmDialogVisible: false,
cmdRule: [
{ message: 'Execute Command should not be empty', required: true }
@@ -101,6 +125,9 @@ export default {
...mapState('spider', [
'spiderForm'
]),
...mapGetters('user', [
'token'
]),
isShowRun () {
if (this.spiderForm.type === 'customized') {
return !!this.spiderForm.cmd
@@ -143,6 +170,16 @@ export default {
},
onSiteSelect (item) {
this.spiderForm.site = item._id
},
onUploadSuccess () {
this.$store.dispatch('file/getFileList', this.spiderForm.src)
this.uploadLoading = false
this.$message.success(this.$t('Uploaded spider files successfully'))
},
onUploadError () {
this.uploadLoading = false
}
}
}
@@ -162,4 +199,8 @@ export default {
.el-autocomplete {
width: 100%;
}
.info-view >>> .el-upload-list {
display: none;
}
</style>

View File

@@ -67,7 +67,7 @@ export default {
.log-view {
margin-top: 0!important;
min-height: 100%;
overflow-y: scroll;
overflow-y: scroll!important;
list-style: none;
color: #A9B7C6;
background: #2B2B2B;

View File

@@ -40,6 +40,7 @@ export default {
// 操作
Add: '添加',
Create: '创建',
Run: '运行',
Deploy: '部署',
Save: '保存',
@@ -63,6 +64,7 @@ export default {
'Item Threshold': '子项阈值',
'Back': '返回',
'New File': '新建文件',
'Rename': '重命名',
// 主页
'Total Tasks': '总任务数',
@@ -234,6 +236,9 @@ export default {
// 文件
'Choose Folder': '选择文件',
'File': '文件',
'Folder': '文件夹',
'Directory': '目录',
// 导入
'Import Spider': '导入爬虫',
@@ -262,6 +267,8 @@ export default {
'Notification': '提示',
'Are you sure to delete this node?': '你确定要删除该节点?',
'Are you sure to run this spider?': '你确定要运行该爬虫?',
'Added spider successfully': '成功添加爬虫',
'Uploaded spider files successfully': '成功上传爬虫文件',
'Node info has been saved successfully': '节点信息已成功保存',
'A task has been scheduled successfully': '已经成功派发一个任务',
'Are you sure to delete this spider?': '你确定要删除该爬虫?',
@@ -270,14 +277,17 @@ export default {
'Do you allow us to collect some statistics to improve Crawlab?': '您允许我们收集统计数据以更好地优化Crawlab',
'Saved file successfully': '成功保存文件',
'An error happened when fetching the data': '请求数据时出错',
'Error when logging in (Please check username and password)': '登录时出错(请检查用户名密码',
'Error when logging in (Please read documentation Q&A)': '登录时出错(请查看文档 Q&A',
'Please enter the correct username': '请输入正确用户名',
'Password length should be no shorter than 5': '密码长度不能小于5',
'Two passwords must be the same': '两个密码必须要一致',
'username already exists': '用户名已存在',
'Deleted successfully': '成功删除',
'Saved successfully': '成功保存',
'Please zip your spider files from the root directory': '爬虫文件请从根目录下开始压缩。',
'Renamed successfully': '重命名保存',
'You can click "Add" to create an empty spider and upload files later.': '您可以点击"添加"按钮创建空的爬虫,之后再上传文件。',
'OR, you can also click "Upload" and upload a zip file containing your spider project.': '或者,您也可以点击"上传"按钮并上传一个包含爬虫项目的 zip 文件。',
'NOTE: When uploading a zip file, please zip your spider files from the ROOT DIRECTORY.': '注意: 上传 zip 文件时,请从 根目录 下开始压缩爬虫文件。',
'English': 'English',
'Are you sure to delete the schedule task?': '确定删除定时任务?',
'Disclaimer': '免责声明',

View File

@@ -13,6 +13,7 @@ import schedule from './modules/schedule'
import lang from './modules/lang'
import site from './modules/site'
import stats from './modules/stats'
import setting from './modules/setting'
import getters from './getters'
Vue.use(Vuex)
@@ -31,6 +32,7 @@ const store = new Vuex.Store({
schedule,
lang,
site,
setting,
// 百度统计
stats
},

View File

@@ -25,8 +25,9 @@ const actions = {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
commit('SET_CURRENT_PATH', path)
request.get(`/spiders/${spiderId}/dir`, { path })
return request.get(`/spiders/${spiderId}/dir`, { path })
.then(response => {
if (!response.data.data) response.data.data = []
commit(
'SET_FILE_LIST',
response.data.data
@@ -38,10 +39,35 @@ const actions = {
getFileContent ({ commit, rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
request.get(`/spiders/${spiderId}/file`, { path })
return request.get(`/spiders/${spiderId}/file`, { path })
.then(response => {
commit('SET_FILE_CONTENT', response.data.data)
})
},
saveFileContent ({ state, rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.post(`/spiders/${spiderId}/file`, { path, content: state.fileContent })
},
addFile ({ rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.put(`/spiders/${spiderId}/file`, { path })
},
addDir ({ rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.put(`/spiders/${spiderId}/dir`, { path })
},
deleteFile ({ rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.delete(`/spiders/${spiderId}/file`, { path })
},
renameFile ({ rootState }, payload) {
const { path, newPath } = payload
const spiderId = rootState.spider.spiderForm._id
return request.post(`/spiders/${spiderId}/file/rename`, { path, new_path: newPath })
}
}

View File

@@ -0,0 +1,28 @@
import request from '../../api/request'
const state = {
setting: {}
}
const getters = {}
const mutations = {
SET_SETTING (state, value) {
state.setting = value
}
}
const actions = {
async getSetting ({ commit }) {
const res = await request.get('/setting')
commit('SET_SETTING', res.data.data)
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}

View File

@@ -174,6 +174,9 @@ const actions = {
addConfigSpider ({ state }) {
return request.put(`/config_spiders`, state.spiderForm)
},
addSpider ({ state }) {
return request.put(`/spiders`, state.spiderForm)
},
async getTemplateList ({ state, commit }) {
const res = await request.get(`/config_spiders_templates`)
commit('SET_TEMPLATE_LIST', res.data.data)

View File

@@ -12,7 +12,8 @@ const state = {
// filter
filter: {
node_id: '',
spider_id: ''
spider_id: '',
status: ''
},
// pagination
pageNum: 1,
@@ -89,7 +90,8 @@ const actions = {
page_num: state.pageNum,
page_size: state.pageSize,
node_id: state.filter.node_id || undefined,
spider_id: state.filter.spider_id || undefined
spider_id: state.filter.spider_id || undefined,
status: state.filter.status || undefined
})
.then(response => {
commit('SET_TASK_LIST', response.data.data || [])

View File

@@ -138,6 +138,11 @@ const user = {
// 编辑用户
editUser ({ state }) {
return request.post(`/users/${state.userForm._id}`, state.userForm)
},
// 添加用户
addUser ({ dispatch, commit, state }) {
return request.put('/users', state.userForm)
}
}
}

View File

@@ -29,13 +29,12 @@ export default {
},
data () {
const converter = new showdown.Converter()
console.log(converter)
return {
converter,
textEn: `
# Disclaimer
This Disclaimer and privacy protection statement (hereinafter referred to as "privacy statement" or "this statement") is applicable to the series of software (hereinafter referred to as "crawlab") developed by crawlab development group (hereinafter referred to as "development group") after you read this statement, if you do not agree with any terms in this statement or have doubts about this statement, please stop using our software immediately. If you have started or are using crawlab, you have read and agree to all terms of this statement.
This Disclaimer and privacy protection statement (hereinafter referred to as "disclaimer statement" or "this statement") is applicable to the series of software (hereinafter referred to as "crawlab") developed by crawlab development group (hereinafter referred to as "development group") after you read this statement, if you do not agree with any terms in this statement or have doubts about this statement, please stop using our software immediately. If you have started or are using crawlab, you have read and agree to all terms of this statement.
1. General: by installing crawlab and using the services and functions provided by crawlab, you have agreed to establish this agreement with the development team. The developer group may at any time change the terms at its sole discretion. The amended "terms" shall take effect automatically as soon as they are published on the GitHub disclaimer page.
2. This product is a distributed crawler management platform based on golang, supporting python, nodejs, go, Java, PHP and other programming languages as well as a variety of crawler frameworks.
@@ -49,7 +48,7 @@ This Disclaimer and privacy protection statement (hereinafter referred to as "pr
textZh: `
# 免责声明
本免责及隐私保护声明(下简称“隐私声明”或“本声明”)适用于 Crawlab 开发组 (以下简称“开发组”)研发的系列软件(以下简称"Crawlab") 在您阅读本声明后若不同意此声明中的任何条款,或对本声明存在质疑,请立刻停止使用我们的软件。若您已经开始或正在使用 Crawlab则表示您已阅读并同意本声明的所有条款之约定。
本免责及隐私保护声明(下简称“免责声明”或“本声明”)适用于 Crawlab 开发组 (以下简称“开发组”)研发的系列软件(以下简称"Crawlab") 在您阅读本声明后若不同意此声明中的任何条款,或对本声明存在质疑,请立刻停止使用我们的软件。若您已经开始或正在使用 Crawlab则表示您已阅读并同意本声明的所有条款之约定。
1. 总则:您通过安装 Crawlab 并使用 Crawlab 提供的服务与功能即表示您已经同意与开发组立本协议。开发组可随时执行全权决定更改“条款”。经修订的“条款”一经在 Github 免责声明页面上公布后,立即自动生效。
2. 本产品是基于Golang的分布式爬虫管理平台支持Python、NodeJS、Go、Java、PHP等多种编程语言以及多种爬虫框架。

View File

@@ -8,9 +8,6 @@
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<!-- <el-dropdown-item>-->
<!-- <span style="display:block;">v0.4.1</span>-->
<!-- </el-dropdown-item>-->
<el-dropdown-item>
<span style="display:block;" @click="logout">{{$t('Logout')}}</span>
</el-dropdown-item>
@@ -31,7 +28,7 @@
</el-dropdown-menu>
</el-dropdown>
<el-dropdown class="documentation right">
<a href="https://docs.crawlab.cn" target="_blank">
<a href="http://docs.crawlab.cn" target="_blank">
<font-awesome-icon :icon="['far', 'question-circle']"/>
<span style="margin-left: 5px;">{{$t('Documentation')}}</span>
</a>

View File

@@ -51,8 +51,12 @@ export default {
},
data () {
return {
version: '0.4.1'
version: ''
}
},
async created () {
const res = await this.$request.get('/version')
this.version = res.data.data
}
}
</script>

View File

@@ -48,7 +48,7 @@
<div class="left">
<span v-if="!isSignUp" class="forgot-password">{{$t('Forgot Password')}}</span>
</div>
<div class="right">
<div class="right" v-if="setting.allow_register === 'Y'">
<span v-if="isSignUp">{{$t('Has Account')}}, </span>
<span v-if="isSignUp" class="sign-in" @click="$router.push('/login')">{{$t('Sign-in')}} ></span>
<span v-if="!isSignUp">{{$t('New to Crawlab')}}, </span>
@@ -57,8 +57,8 @@
</div>
<div class="tips">
<span>{{$t('Initial Username/Password')}}: admin/admin</span>
<a href="https://github.com/tikazyq/crawlab" target="_blank" style="float:right">
<img src="https://img.shields.io/badge/github-crawlab-blue">
<a href="https://github.com/crawlab-team/crawlab" target="_blank" style="float:right">
<img src="https://img.shields.io/github/stars/crawlab-team/crawlab?logo=github">
</a>
</div>
<div class="lang">
@@ -116,6 +116,9 @@ export default {
}
},
computed: {
...mapState('setting', [
'setting'
]),
...mapState('lang', [
'lang'
]),
@@ -136,7 +139,7 @@ export default {
this.$router.push({ path: this.redirect || '/' })
this.$store.dispatch('user/getInfo')
}).catch(() => {
this.$message.error(this.$t('Error when logging in (Please check username and password)'))
this.$message.error(this.$t('Error when logging in (Please read documentation Q&A)'))
this.loading = false
})
}

View File

@@ -34,6 +34,49 @@
:visible.sync="addDialogVisible"
:before-close="onAddDialogClose">
<el-tabs :active-name="spiderType">
<el-tab-pane name="customized" :label="$t('Customized')">
<el-form :model="spiderForm" ref="addCustomizedForm" inline-message label-width="120px">
<el-form-item :label="$t('Spider Name')" prop="name" required>
<el-input v-model="spiderForm.name" :placeholder="$t('Spider Name')"/>
</el-form-item>
<el-form-item :label="$t('Display Name')" prop="display_name" required>
<el-input v-model="spiderForm.display_name" :placeholder="$t('Display Name')"/>
</el-form-item>
<el-form-item :label="$t('Execute Command')" prop="cmd" required>
<el-input v-model="spiderForm.cmd" :placeholder="$t('Execute Command')"/>
</el-form-item>
<el-form-item :label="$t('Results')" prop="col" required>
<el-input v-model="spiderForm.col" :placeholder="$t('Results')"/>
</el-form-item>
<el-form-item :label="$t('Upload Zip File')" label-width="120px" name="site">
<el-upload
:action="$request.baseUrl + '/spiders'"
:headers="{Authorization:token}"
:on-success="onUploadSuccess"
:file-list="fileList">
<el-button size="normal" type="primary" icon="el-icon-upload"
style="width: 160px; font-size: 18px;font-weight: bolder">
{{$t('Upload')}}
</el-button>
</el-upload>
</el-form-item>
</el-form>
<el-alert
type="warning"
:closable="false"
style="margin-bottom: 10px"
>
<p>{{$t('You can click "Add" to create an empty spider and upload files later.')}}</p>
<p>{{$t('OR, you can also click "Upload" and upload a zip file containing your spider project.')}}</p>
<p style="font-weight: bolder">
<i class="fa fa-exclamation-triangle"></i> {{$t('NOTE: When uploading a zip file, please zip your' +
' spider files from the ROOT DIRECTORY.')}}
</p>
</el-alert>
<div class="actions">
<el-button type="primary" @click="onAddCustomized">{{$t('Add')}}</el-button>
</div>
</el-tab-pane>
<el-tab-pane name="configurable" :label="$t('Configurable')">
<el-form :model="spiderForm" ref="addConfigurableForm" inline-message label-width="120px">
<el-form-item :label="$t('Spider Name')" prop="name" required>
@@ -60,63 +103,10 @@
<el-button type="primary" @click="onAddConfigurable">{{$t('Add')}}</el-button>
</div>
</el-tab-pane>
<el-tab-pane name="customized" :label="$t('Customized')">
<el-form :model="spiderForm" ref="addCustomizedForm" inline-message>
<el-form-item :label="$t('Upload Zip File')" label-width="120px" name="site">
<el-upload
:action="$request.baseUrl + '/spiders'"
:headers="{Authorization:token}"
:on-change="onUploadChange"
:on-success="onUploadSuccess"
:file-list="fileList">
<el-button size="small" type="primary" icon="el-icon-upload">{{$t('Upload')}}</el-button>
</el-upload>
</el-form-item>
</el-form>
<el-alert type="error" :title="$t('Please zip your spider files from the root directory')"
:closable="false"></el-alert>
</el-tab-pane>
</el-tabs>
</el-dialog>
<!--./add dialog-->
<!--configurable spider dialog-->
<el-dialog :title="$t('Add Configurable Spider')"
width="40%"
:visible.sync="addConfigurableDialogVisible"
:before-close="onAddConfigurableDialogClose">
<el-form :model="spiderForm" ref="addConfigurableForm" inline-message>
<el-form-item :label="$t('Spider Name')" label-width="120px" prop="name" required>
<el-input :placeholder="$t('Spider Name')" v-model="spiderForm.name"></el-input>
</el-form-item>
<el-form-item :label="$t('Results Collection')" label-width="120px" name="col">
<el-input :placeholder="$t('Results Collection')" v-model="spiderForm.col"></el-input>
</el-form-item>
<el-form-item :label="$t('Site')" label-width="120px" name="site">
<el-autocomplete v-model="spiderForm.site"
:placeholder="$t('Site')"
:fetch-suggestions="fetchSiteSuggestions"
@select="onAddConfigurableSiteSelect">
</el-autocomplete>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="addConfigurableDialogVisible = false">{{$t('Cancel')}}</el-button>
<el-button v-loading="addConfigurableLoading" type="primary"
@click="onAddConfigurableSpider">{{$t('Add')}}</el-button>
</span>
</el-dialog>
<!--./configurable spider dialog-->
<!--customized spider dialog-->
<el-dialog :title="$t('Add Customized Spider')"
width="40%"
:visible.sync="addCustomizedDialogVisible"
:before-close="onAddCustomizedDialogClose">
</el-dialog>
<!--./customized spider dialog-->
<!--crawl confirm dialog-->
<crawl-confirm-dialog
:visible="crawlConfirmDialogVisible"
@@ -154,10 +144,14 @@
<el-button size="small" v-if="false" type="primary" icon="fa fa-download" @click="openImportDialog">
{{$t('Import Spiders')}}
</el-button>
<el-button size="small" type="success"
icon="el-icon-plus"
class="btn add"
@click="onAdd">
<el-button
size="normal"
type="success"
icon="el-icon-plus"
class="btn add"
@click="onAdd"
style="font-weight: bolder"
>
{{$t('Add Spider')}}
</el-button>
@@ -299,8 +293,6 @@ export default {
isEditMode: false,
dialogVisible: false,
addDialogVisible: false,
addConfigurableDialogVisible: false,
addCustomizedDialogVisible: false,
crawlConfirmDialogVisible: false,
activeSpiderId: undefined,
filter: {
@@ -320,7 +312,7 @@ export default {
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
},
fileList: [],
spiderType: 'configurable'
spiderType: 'customized'
}
},
computed: {
@@ -374,9 +366,19 @@ export default {
})
},
onAddCustomized () {
this.addDialogVisible = false
this.addCustomizedDialogVisible = true
this.$st.sendEv('爬虫列表', '添加爬虫', '自定义爬虫')
this.$refs['addCustomizedForm'].validate(async res => {
if (!res) return
let res2
try {
res2 = await this.$store.dispatch('spider/addSpider')
} catch (e) {
this.$message.error(this.$t('Something wrong happened'))
return
}
await this.$store.dispatch('spider/getSpiderList')
this.$router.push(`/spiders/${res2.data.data._id}`)
this.$st.sendEv('爬虫列表', '添加爬虫', '自定义爬虫')
})
},
onRefresh () {
this.getList()
@@ -510,9 +512,7 @@ export default {
}
})
},
onUploadChange () {
},
onUploadSuccess () {
onUploadSuccess (res) {
// clear fileList
this.fileList = []
@@ -521,8 +521,11 @@ export default {
this.getList()
}, 500)
// close popup
this.addCustomizedDialogVisible = false
// message
this.$message.success(this.$t('Uploaded spider files successfully'))
// navigate to spider detail
this.$router.push(`/spiders/${res.data._id}`)
},
getTime (str) {
if (!str || str.match('^0001')) return 'NA'

View File

@@ -4,6 +4,29 @@
<!--filter-->
<div class="filter">
<div class="left">
<el-form model="filter" label-width="100px" label-position="right" inline>
<el-form-item prop="node_id" :label="$t('Node')">
<el-select v-model="filter.node_id" size="small" :placeholder="$t('Node')" @change="onFilterChange">
<el-option value="" :label="$t('All')"/>
<el-option v-for="node in nodeList" :key="node._id" :value="node._id" :label="node.name"/>
</el-select>
</el-form-item>
<el-form-item prop="spider_id" :label="$t('Spider')">
<el-select v-model="filter.spider_id" size="small" :placeholder="$t('Spider')" @change="onFilterChange">
<el-option value="" :label="$t('All')"/>
<el-option v-for="spider in spiderList" :key="spider._id" :value="spider._id" :label="spider.name"/>
</el-select>
</el-form-item>
<el-form-item prop="status" :label="$t('Status')">
<el-select v-model="filter.status" size="small" :placeholder="$t('Status')" @change="onFilterChange">
<el-option value="" :label="$t('All')"></el-option>
<el-option value="finished" :label="$t('Finished')"></el-option>
<el-option value="running" :label="$t('Running')"></el-option>
<el-option value="error" :label="$t('Error')"></el-option>
<el-option value="cancelled" :label="$t('Cancelled')"></el-option>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="right">
<el-button @click="onRemoveMultipleTask" size="small" type="danger">
@@ -310,6 +333,10 @@ export default {
},
onSelectionChange (val) {
this.multipleSelection = val
},
onFilterChange () {
this.$store.dispatch('task/getTaskList')
this.$st.sendEv('任务列表', '筛选任务')
}
},
created () {

View File

@@ -3,14 +3,14 @@
<!--dialog-->
<el-dialog :visible.sync="dialogVisible" :title="$t('Edit User')">
<el-form ref="form" :model="userForm" label-width="80px" :rules="rules" inline-message>
<el-form-item :label="$t('Username')">
<el-input v-model="userForm.username" disabled></el-input>
<el-form-item prop="username" :label="$t('Username')" required>
<el-input v-model="userForm.username" :placeholder="$t('Username')" :disabled="!isAdd"></el-input>
</el-form-item>
<el-form-item prop="password" :label="$t('Password')">
<el-form-item prop="password" :label="$t('Password')" required>
<el-input type="password" v-model="userForm.password" :placeholder="$t('Password')"></el-input>
</el-form-item>
<el-form-item :label="$t('Role')">
<el-select v-model="userForm.role">
<el-form-item prop="role" :label="$t('Role')" required>
<el-select v-model="userForm.role" :placeholder="$t('Role')">
<el-option value="admin" :label="$t('admin')"></el-option>
<el-option value="normal" :label="$t('normal')"></el-option>
</el-select>
@@ -27,7 +27,7 @@
<div class="filter">
<div class="left"></div>
<div class="right">
<!--<el-button type="primary" size="small">新增用户</el-button>-->
<el-button type="success" icon="el-icon-plus" size="small" @click="onClickAddUser">添加用户</el-button>
</div>
</div>
<!--table-->
@@ -109,6 +109,7 @@ export default {
}
return {
dialogVisible: false,
isAdd: false,
rules: {
password: [{ validator: validatePass }]
}
@@ -145,6 +146,7 @@ export default {
return dayjs(ts).format('YYYY-MM-DD HH:mm:ss')
},
onEdit (row) {
this.isAdd = false
this.$store.commit('user/SET_USER_FORM', row)
this.dialogVisible = true
},
@@ -161,24 +163,48 @@ export default {
message: this.$t('Deleted successfully')
})
})
.then(() => {
this.$store.dispatch('user/getUserList')
})
this.$st.sendEv('用户列表', '删除用户')
})
// this.$store.commit('user/SET_USER_FORM', row)
},
onConfirm () {
this.dialogVisible = false
this.$refs.form.validate(valid => {
if (valid) {
if (!valid) return
if (this.isAdd) {
// 添加用户
this.$store.dispatch('user/addUser')
.then(() => {
this.$message({
type: 'success',
message: this.$t('Saved successfully')
})
this.dialogVisible = false
this.$st.sendEv('用户列表', '添加用户')
})
.then(() => {
this.$store.dispatch('user/getUserList')
})
} else {
// 编辑用户
this.$store.dispatch('user/editUser')
.then(() => {
this.$message({
type: 'success',
message: this.$t('Saved successfully')
})
this.dialogVisible = false
this.$st.sendEv('用户列表', '编辑用户')
})
}
})
this.$st.sendEv('用户列表', '编辑用户')
},
onClickAddUser () {
this.isAdd = true
this.$store.commit('user/SET_USER_FORM', {})
this.dialogVisible = true
}
},
created () {
@@ -192,15 +218,18 @@ export default {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
.filter-search {
width: 240px;
}
.right {
.btn {
margin-left: 10px;
}
}
.filter-search {
width: 240px;
}
.right {
.btn {
margin-left: 10px;
}
}
}
.el-table {

View File

@@ -1336,7 +1336,7 @@ async@^1.5.2:
version "1.5.2"
resolved "http://registry.npm.taobao.org/async/download/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
async@^2.1.4, async@^2.5.0:
async@^2.1.4:
version "2.6.2"
resolved "http://registry.npm.taobao.org/async/download/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381"
dependencies:
@@ -2199,6 +2199,11 @@ commander@^2.18.0, commander@^2.19.0:
version "2.19.0"
resolved "http://registry.npm.taobao.org/commander/download/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commondir@^1.0.1:
version "1.0.1"
resolved "http://registry.npm.taobao.org/commondir/download/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -4031,10 +4036,11 @@ handle-thing@^2.0.0:
resolved "http://registry.npm.taobao.org/handle-thing/download/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754"
handlebars@^4.0.3:
version "4.1.0"
resolved "http://registry.npm.taobao.org/handlebars/download/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a"
version "4.5.3"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482"
integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==
dependencies:
async "^2.5.0"
neo-async "^2.6.0"
optimist "^0.6.1"
source-map "^0.6.1"
optionalDependencies:
@@ -5727,7 +5733,8 @@ minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
minimist@~0.0.1:
version "0.0.10"
resolved "http://registry.npm.taobao.org/minimist/download/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
minipass@^2.2.1, minipass@^2.3.4:
version "2.3.5"
@@ -5863,8 +5870,9 @@ negotiator@0.6.1:
resolved "http://registry.npm.taobao.org/negotiator/download/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
neo-async@^2.5.0, neo-async@^2.6.0:
version "2.6.0"
resolved "http://registry.npm.taobao.org/neo-async/download/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835"
version "2.6.1"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
nice-try@^1.0.4:
version "1.0.5"
@@ -6205,7 +6213,8 @@ opn@^5.1.0, opn@^5.3.0:
optimist@^0.6.1:
version "0.6.1"
resolved "http://registry.npm.taobao.org/optimist/download/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
dependencies:
minimist "~0.0.1"
wordwrap "~0.0.2"
@@ -7745,7 +7754,8 @@ source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7:
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
version "0.6.1"
resolved "http://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
spdx-correct@^3.0.0:
version "3.1.0"
@@ -8328,13 +8338,21 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "http://registry.npm.taobao.org/typedarray/download/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
uglify-js@3.4.x, uglify-js@^3.1.4:
uglify-js@3.4.x:
version "3.4.9"
resolved "http://registry.npm.taobao.org/uglify-js/download/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
dependencies:
commander "~2.17.1"
source-map "~0.6.1"
uglify-js@^3.1.4:
version "3.7.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"
integrity sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==
dependencies:
commander "~2.20.3"
source-map "~0.6.1"
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "http://registry.npm.taobao.org/unicode-canonical-property-names-ecmascript/download/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -8862,7 +8880,8 @@ wide-align@^1.1.0:
wordwrap@~0.0.2:
version "0.0.3"
resolved "http://registry.npm.taobao.org/wordwrap/download/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
wordwrap@~1.0.0:
version "1.0.0"