Full Code of openspug/spug for AI

3.0 774d10f26da8 cached
362 files
958.3 KB
257.2k tokens
936 symbols
1 requests
Download .txt
Showing preview only (1,045K chars total). Download the full file or copy to clipboard to get everything.
Repository: openspug/spug
Branch: 3.0
Commit: 774d10f26da8
Files: 362
Total size: 958.3 KB

Directory structure:
gitextract_da93zxoy/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   └── bug-report.md
│   └── workflows/
│       └── github_to_gitee.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs/
│   ├── FQA.md
│   ├── docker/
│   │   ├── Dockerfile
│   │   ├── docker-compose.yml
│   │   ├── entrypoint.sh
│   │   ├── init_spug
│   │   ├── nginx.conf
│   │   ├── redis.conf
│   │   ├── spug.ini
│   │   └── ssh_config
│   └── install.sh
├── spug_api/
│   ├── .gitignore
│   ├── apps/
│   │   ├── account/
│   │   │   ├── __init__.py
│   │   │   ├── history.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       ├── set.py
│   │   │   │       ├── update.py
│   │   │   │       ├── updatedb.py
│   │   │   │       └── user.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── alarm/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   ├── apis/
│   │   │   ├── __init__.py
│   │   │   ├── config.py
│   │   │   ├── deploy.py
│   │   │   └── urls.py
│   │   ├── app/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── config/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── deploy/
│   │   │   ├── __init__.py
│   │   │   ├── helper.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── exec/
│   │   │   ├── __init__.py
│   │   │   ├── executors.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       └── runworker.py
│   │   │   ├── models.py
│   │   │   ├── transfer.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   ├── file/
│   │   │   ├── __init__.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── home/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── navigation.py
│   │   │   ├── notice.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   ├── host/
│   │   │   ├── __init__.py
│   │   │   ├── add.py
│   │   │   ├── extend.py
│   │   │   ├── group.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── monitor/
│   │   │   ├── __init__.py
│   │   │   ├── executors.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       └── runmonitor.py
│   │   │   ├── models.py
│   │   │   ├── scheduler.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── notify/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   ├── repository/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── schedule/
│   │   │   ├── __init__.py
│   │   │   ├── builtin.py
│   │   │   ├── executors.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       └── runscheduler.py
│   │   │   ├── models.py
│   │   │   ├── scheduler.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   └── setting/
│   │       ├── __init__.py
│   │       ├── models.py
│   │       ├── urls.py
│   │       ├── user.py
│   │       ├── utils.py
│   │       └── views.py
│   ├── consumer/
│   │   ├── __init__.py
│   │   ├── consumers.py
│   │   ├── routing.py
│   │   └── utils.py
│   ├── libs/
│   │   ├── __init__.py
│   │   ├── channel.py
│   │   ├── decorators.py
│   │   ├── gitlib.py
│   │   ├── helper.py
│   │   ├── ldap.py
│   │   ├── mail.py
│   │   ├── middleware.py
│   │   ├── mixins.py
│   │   ├── parser.py
│   │   ├── push.py
│   │   ├── spug.py
│   │   ├── ssh.py
│   │   ├── utils.py
│   │   └── validators.py
│   ├── logs/
│   │   └── .gitkeep
│   ├── manage.py
│   ├── repos/
│   │   ├── .gitkeep
│   │   └── build/
│   │       └── .gitkeep
│   ├── requirements.txt
│   ├── spug/
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── routing.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── storage/
│   │   └── transfer/
│   │       └── .gitkeep
│   └── tools/
│       ├── migrate.py
│       ├── start-api.sh
│       ├── start-monitor.sh
│       ├── start-scheduler.sh
│       ├── start-worker.sh
│       ├── start-ws.sh
│       └── supervisor-spug.ini
└── spug_web/
    ├── .gitignore
    ├── README.md
    ├── config-overrides.js
    ├── jsconfig.json
    ├── package.json
    ├── public/
    │   ├── index.html
    │   ├── manifest.json
    │   ├── resource/
    │   │   └── 主机导入模板.xlsx
    │   └── robots.txt
    └── src/
        ├── App.js
        ├── components/
        │   ├── ACEditor.js
        │   ├── Action.js
        │   ├── AppSelector.js
        │   ├── AuthButton.js
        │   ├── AuthCard.js
        │   ├── AuthDiv.js
        │   ├── AuthFragment.js
        │   ├── Breadcrumb.js
        │   ├── Link.js
        │   ├── LinkButton.js
        │   ├── NotFound.js
        │   ├── SearchForm.js
        │   ├── StatisticsCard.js
        │   ├── TableCard.js
        │   ├── index.js
        │   └── index.module.less
        ├── gStore.js
        ├── index.js
        ├── index.less
        ├── layout/
        │   ├── Footer.js
        │   ├── Header.js
        │   ├── Notification.js
        │   ├── Sider.js
        │   ├── index.js
        │   └── layout.module.less
        ├── libs/
        │   ├── functools.js
        │   ├── history.js
        │   ├── http.js
        │   ├── index.js
        │   ├── libs.module.css
        │   └── router.js
        ├── pages/
        │   ├── alarm/
        │   │   ├── alarm/
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── contact/
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   └── group/
        │   │       ├── Form.js
        │   │       ├── Table.js
        │   │       ├── index.js
        │   │       └── store.js
        │   ├── config/
        │   │   ├── app/
        │   │   │   ├── Form.js
        │   │   │   ├── Rel.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── environment/
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── service/
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   └── setting/
        │   │       ├── DiffConfig.js
        │   │       ├── Form.js
        │   │       ├── JSONView.js
        │   │       ├── Record.js
        │   │       ├── TableView.js
        │   │       ├── TextView.js
        │   │       ├── index.js
        │   │       ├── index.module.css
        │   │       └── store.js
        │   ├── dashboard/
        │   │   ├── AlarmTrend.js
        │   │   ├── RequestTop.js
        │   │   ├── StatisticCard.js
        │   │   ├── index.js
        │   │   └── index.module.css
        │   ├── deploy/
        │   │   ├── app/
        │   │   │   ├── AddSelect.js
        │   │   │   ├── AutoDeploy.js
        │   │   │   ├── CloneConfirm.js
        │   │   │   ├── Ext1Form.js
        │   │   │   ├── Ext1Setup1.js
        │   │   │   ├── Ext1Setup2.js
        │   │   │   ├── Ext1Setup3.js
        │   │   │   ├── Ext2Form.js
        │   │   │   ├── Ext2Setup1.js
        │   │   │   ├── Ext2Setup2.js
        │   │   │   ├── Form.js
        │   │   │   ├── Repo.js
        │   │   │   ├── Table.js
        │   │   │   ├── Tips.js
        │   │   │   ├── index.js
        │   │   │   ├── index.module.css
        │   │   │   └── store.js
        │   │   ├── repository/
        │   │   │   ├── Console.js
        │   │   │   ├── Detail.js
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   ├── index.module.less
        │   │   │   └── store.js
        │   │   └── request/
        │   │       ├── Approve.js
        │   │       ├── BatchDelete.js
        │   │       ├── Ext1Console.js
        │   │       ├── Ext1Form.js
        │   │       ├── Ext2Console.js
        │   │       ├── Ext2Form.js
        │   │       ├── HostSelector.js
        │   │       ├── OutView.js
        │   │       ├── Rollback.js
        │   │       ├── Table.js
        │   │       ├── index.js
        │   │       ├── index.module.less
        │   │       └── store.js
        │   ├── exec/
        │   │   ├── task/
        │   │   │   ├── Output.js
        │   │   │   ├── Parameter.js
        │   │   │   ├── TemplateSelector.js
        │   │   │   ├── index.js
        │   │   │   ├── index.module.less
        │   │   │   └── store.js
        │   │   ├── template/
        │   │   │   ├── Form.js
        │   │   │   ├── Parameter.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   └── transfer/
        │   │       ├── Output.js
        │   │       ├── index.js
        │   │       ├── index.module.less
        │   │       └── store.js
        │   ├── home/
        │   │   ├── Nav.js
        │   │   ├── NavForm.js
        │   │   ├── Notice.js
        │   │   ├── Todo.js
        │   │   ├── index.js
        │   │   └── index.module.less
        │   ├── host/
        │   │   ├── BatchSync.js
        │   │   ├── CloudImport.js
        │   │   ├── Detail.js
        │   │   ├── Form.js
        │   │   ├── Group.js
        │   │   ├── IPAddress.js
        │   │   ├── Import.js
        │   │   ├── Selector.js
        │   │   ├── Sync.js
        │   │   ├── Table.js
        │   │   ├── icons/
        │   │   │   └── index.js
        │   │   ├── index.js
        │   │   ├── index.module.less
        │   │   ├── selector.module.less
        │   │   ├── store.js
        │   │   └── store2.js
        │   ├── login/
        │   │   ├── index.js
        │   │   └── login.module.css
        │   ├── monitor/
        │   │   ├── Form.js
        │   │   ├── MonitorCard.js
        │   │   ├── Step1.js
        │   │   ├── Step2.js
        │   │   ├── Table.js
        │   │   ├── index.js
        │   │   ├── index.module.less
        │   │   └── store.js
        │   ├── schedule/
        │   │   ├── Form.js
        │   │   ├── Info.js
        │   │   ├── Record.js
        │   │   ├── Step1.js
        │   │   ├── Step2.js
        │   │   ├── Step3.js
        │   │   ├── Table.js
        │   │   ├── index.js
        │   │   ├── index.module.css
        │   │   └── store.js
        │   ├── ssh/
        │   │   ├── FileManager.js
        │   │   ├── Setting.js
        │   │   ├── Terminal.js
        │   │   ├── index.js
        │   │   ├── index.module.less
        │   │   ├── setting.module.less
        │   │   └── themes.js
        │   ├── system/
        │   │   ├── account/
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── login/
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── role/
        │   │   │   ├── DeployPerm.js
        │   │   │   ├── Form.js
        │   │   │   ├── HostPerm.js
        │   │   │   ├── PagePerm.js
        │   │   │   ├── RoleUsers.js
        │   │   │   ├── Table.js
        │   │   │   ├── codes.js
        │   │   │   ├── index.js
        │   │   │   ├── index.module.css
        │   │   │   └── store.js
        │   │   └── setting/
        │   │       ├── About.js
        │   │       ├── AlarmSetting.js
        │   │       ├── KeySetting.js
        │   │       ├── LDAPSetting.js
        │   │       ├── OpenService.js
        │   │       ├── PushSetting.js
        │   │       ├── SecuritySetting.js
        │   │       ├── index.js
        │   │       ├── index.module.css
        │   │       └── store.js
        │   └── welcome/
        │       ├── index/
        │       │   └── index.js
        │       └── info/
        │           ├── Basic.js
        │           ├── Reset.js
        │           ├── index.js
        │           ├── index.module.css
        │           └── store.js
        ├── routes.js
        ├── serviceWorker.js
        └── setupProxy.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
custom: ['https://www.spug.dev/sponsorship/']


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: "🐛 Bug Report"
about: Report a reproducible bug or regression.
title: 'Bug: '
---

<!--
  Spug 版本信息可以在 系统管理/系统设置/关于 中查看,请填写 Spug 版本信息。
-->

Spug 版本:

## 问题重现步骤

1.
2.

## 报错/问题截图


## 期望的结果



================================================
FILE: .github/workflows/github_to_gitee.yml
================================================
name: github repos to gitee job
on:
# 如果需要PR触发把push前的#去掉
# push:
  schedule:
    # 每天北京时间1点跑
    - cron:  '0 1 * * *'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Mirror the Github organization repos to Gitee.
      uses: Yikun/gitee-mirror-action@v0.01
      with:
        src: github/openspug
        dst: gitee/openspug
        # Gitee对应的秘钥
        private_key: ${{ secrets.mac_pro_videojj }}
        # 需要同步的Github组织名(源)
        github_org: openspug
        # 需要同步到的Gitee的组织名(目的)
        gitee_org: openspug


================================================
FILE: .gitignore
================================================
/.idea/


================================================
FILE: LICENSE
================================================
                    GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.

  A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate.  Many developers of free software are heartened and
encouraged by the resulting cooperation.  However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.

  The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community.  It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server.  Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.

  An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals.  This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU Affero General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Remote Network Interaction; Use with the GNU General Public License.

  Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software.  This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time.  Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source.  For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code.  There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.


================================================
FILE: README.md
================================================
<h1 align="center">Spug</h1>

<div align="center">

Spug是面向中小型企业设计的轻量级无Agent的自动化运维平台,整合了主机管理、主机批量执行、主机在线终端、应用发布部署、在线任务计划、配置中心、监控、报警等一系列功能。

</div>

- 公司官网:https://www.spug.cc
- 项目官网:https://ops.spug.cc
- 使用文档:https://ops.spug.cc/docs/about-spug/

## 演示环境

演示地址:https://demo.spug.cc

## 🔐免费通配符SSL证书
免费通配符,付费证书价格亲民,性价比超高,低于市场其他平台价格,免费专家一对一配置服务,购买流程简单快速,且支持7天无理由退款和开具发票。提供一键下载和SSL过期通知配置,免费申请:[https://ssl.spug.cc](https://ssl.spug.cc)


## 🔥推送助手

推送助手是一个集成了电话、短信、邮件、飞书、钉钉、微信、企业微信等多通道的消息推送平台,可以3分钟实现个人电话短信推送,点击体验:[https://push.spug.cc](https://push.spug.cc)


## 特性

- **批量执行**: 主机命令在线批量执行
- **在线终端**: 主机支持浏览器在线终端登录
- **文件管理**: 主机文件在线上传下载
- **任务计划**: 灵活的在线任务计划
- **发布部署**: 支持自定义发布部署流程
- **配置中心**: 支持KV、文本、json等格式的配置
- **监控中心**: 支持站点、端口、进程、自定义等监控
- **报警中心**: 支持短信、邮件、钉钉、微信等报警方式
- **优雅美观**: 基于 Ant Design 的UI界面
- **开源免费**: 前后端代码完全开源


## 环境

* Python 3.6+
* Django 2.2
* Node 12.14
* React 16.11

## 安装

[官方文档](https://ops.spug.cc/docs/install-docker)

更多使用帮助请参考: [使用文档](https://ops.spug.cc/docs/host-manage/)


## 推荐项目
[Yearning — MYSQL 开源SQL语句审核平台](https://github.com/cookieY/Yearning)


## 预览

### 主机管理
![image](https://cdn.spug.cc/img/3.0/host.jpg)

#### 主机在线终端
![image](https://cdn.spug.cc/img/3.0/web-terminal.jpg)

#### 文件在线上传下载
![image](https://cdn.spug.cc/img/3.0/file-manager.jpg)

#### 主机批量执行
![image](https://cdn.spug.cc/img/3.0/host-exec.jpg)
![image](https://cdn.spug.cc/img/3.0/host-exec2.jpg)

#### 应用发布
![image](https://cdn.spug.cc/img/3.0/deploy.jpg)

#### 监控报警
![image](https://cdn.spug.cc/img/3.0/monitor.jpg)

#### 角色权限
![image](https://cdn.spug.cc/img/3.0/user-role.jpg)


## 赞助
<table>
  <thead>
    <tr>
      <th align="center" style="width: 115px;">
        <a href="https://www.ucloud.cn/site/active/kuaijie.html?invitation_code=C1xD0E5678FBA77">
          <img src="https://cdn.spug.cc/img/ucloud.png" width="115px"><br>
          <sub>UCloud</sub><br>
          <sub>5 元/月云主机</sub>
        </a>
      </th>
        <th align="center" style="width: 115px;">
        <a href="https://www.aliyun.com/minisite/goods?userCode=bkj6b9tn">
          <img src="https://cdn.spug.cc/img/aliyun-logo.png" width="115px"><br>
          <sub>阿里云</sub><br>
          <sub>2核心2G低至99元/年</sub>
        </a>
      </th>
      <th align="center" style="width: 125px;">
        <a href="http://www.magedu.com">
          <img src="https://cdn.spug.cc/img/magedu-logo.jpeg" width="115px"><br>
          <sub>马哥教育</sub><br>
          <sub>IT人高薪职业学院</sub>
        </a>
      </th>
    </tr>
  </thead>
</table>

## 开发者群
#### 关注Spug运维公众号加微信群、QQ群、获取最新产品动态
<div >
   <img src="https://cdn.spug.cc/img/spug-club.jpg" width = "300" height = "300" alt="spug-qq" align=center />
<div>
  
## License & Copyright
[AGPL-3.0](https://opensource.org/licenses/AGPL-3.0)


================================================
FILE: docs/FQA.md
================================================
### install mysqlclient
```shell
# for centos 7
yum install mariadb-devel python3-devel gcc
pip install mysqlclient
```


================================================
FILE: docs/docker/Dockerfile
================================================
FROM centos:7.9.2009

ENV TZ=Asia/Shanghai
RUN yum install -y epel-release https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm && yum install -y --setopt=tsflags=nodocs nginx redis mariadb-devel python36 python36-devel openldap-devel supervisor git gcc wget unzip net-tools sshpass rsync sshfs && yum -y clean all --enablerepo='*'

RUN pip3 install --no-cache-dir --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/
RUN pip3 install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ \
    gunicorn \
    mysqlclient \
    cryptography==36.0.2 \
    apscheduler==3.7.0 \
    asgiref==3.2.10 \
    Django==2.2.28 \
    channels==2.3.1 \
    channels_redis==2.4.1 \
    paramiko==2.11.0 \
    django-redis==4.10.0 \
    requests==2.22.0 \
    GitPython==3.0.8 \
    python-ldap==3.4.0 \
    openpyxl==3.0.3 \
    user_agents==2.2.0

RUN localedef -c -i en_US -f UTF-8 en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8
RUN echo -e '\n# Source definitions\n. /etc/profile\n' >> /root/.bashrc
RUN mkdir -p /data/repos
COPY init_spug /usr/bin/
COPY nginx.conf /etc/nginx/
COPY ssh_config /etc/ssh/
COPY spug.ini /etc/supervisord.d/
COPY redis.conf /etc/
COPY entrypoint.sh /

VOLUME /data
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]


================================================
FILE: docs/docker/docker-compose.yml
================================================
version: "3.3"
services:
  db:
    image: mariadb:10.8
    container_name: spug-db
    restart: always
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - /data/spug/mysql:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=spug
      - MYSQL_USER=spug
      - MYSQL_PASSWORD=spug.cc
      - MYSQL_ROOT_PASSWORD=spug.cc
  spug:
    image: openspug/spug-service
    container_name: spug
    privileged: true
    restart: always
    volumes:
      - /data/spug/service:/data/spug
      - /data/spug/repos:/data/repos
    ports:
      # 如果80端口被占用可替换为其他端口,例如: - "8000:80"
      - "80:80"
    environment:
      - SPUG_DOCKER_VERSION=v3.2.4
      - MYSQL_DATABASE=spug
      - MYSQL_USER=spug
      - MYSQL_PASSWORD=spug.cc
      - MYSQL_HOST=db
      - MYSQL_PORT=3306
    depends_on:
      - db


================================================
FILE: docs/docker/entrypoint.sh
================================================
#!/bin/bash
#
set -e

if [ -e /root/.bashrc ]; then
    source /root/.bashrc
fi

if [ ! -d /data/spug/spug_api ]; then
    git clone -b $SPUG_DOCKER_VERSION https://gitee.com/openspug/spug.git /data/spug
    curl -o web.tar.gz https://cdn.spug.cc/spug/web_${SPUG_DOCKER_VERSION}.tar.gz
    tar xf web.tar.gz -C /data/spug/spug_web/
    rm -f web.tar.gz
    SECRET_KEY=$(< /dev/urandom tr -dc '!@#%^.a-zA-Z0-9' | head -c50)
    cat > /data/spug/spug_api/spug/overrides.py << EOF
import os


DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1']
SECRET_KEY = '${SECRET_KEY}'

DATABASES = {
    'default': {
        'ATOMIC_REQUESTS': True,
        'ENGINE': 'django.db.backends.mysql',
        'NAME': os.environ.get('MYSQL_DATABASE'),
        'USER': os.environ.get('MYSQL_USER'),
        'PASSWORD': os.environ.get('MYSQL_PASSWORD'),
        'HOST': os.environ.get('MYSQL_HOST'),
        'PORT': os.environ.get('MYSQL_PORT'),
        'OPTIONS': {
            'charset': 'utf8mb4',
            'sql_mode': 'STRICT_TRANS_TABLES',
        }
    }
}
EOF
fi

exec supervisord -c /etc/supervisord.conf


================================================
FILE: docs/docker/init_spug
================================================
#!/bin/bash
#
set -e
set -u

python3 /data/spug/spug_api/manage.py updatedb
python3 /data/spug/spug_api/manage.py user add -u $1 -p $2 -n 管理员 -s


================================================
FILE: docs/docker/nginx.conf
================================================
# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;
    server_tokens       off;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  _;
        root         /data/spug/spug_web/build;
	client_max_body_size	0;
        add_header   X-Frame-Options SAMEORIGIN always;

	gzip  on;
	gzip_min_length  1k;
	gzip_buffers     4 16k;
	gzip_http_version 1.1;
	gzip_comp_level 7;
	gzip_types       text/plain text/css text/javascript application/javascript application/json;
	gzip_disable "MSIE [1-6]\.";
	gzip_vary on;

        location ^~ /api/ {
                rewrite ^/api(.*) $1 break;
                proxy_pass http://127.0.0.1:9001;
		proxy_read_timeout 180s;
                proxy_redirect off;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location ^~ /api/ws/ {
                rewrite ^/api(.*) $1 break;
                proxy_pass http://127.0.0.1:9002;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "Upgrade";
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
       
        location / {
                try_files $uri /index.html;
        }
    }
}


================================================
FILE: docs/docker/redis.conf
================================================
# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

################################## INCLUDES ###################################

# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Notice option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf

################################## NETWORK #####################################

# By default, if no "bind" configuration directive is specified, Redis listens
# for connections from all the network interfaces available on the server.
# It is possible to listen to just one or multiple selected interfaces using
# the "bind" configuration directive, followed by one or more IP addresses.
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 lookback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bind 127.0.0.1

# Protected mode is a layer of security protection, in order to avoid that
# Redis instances left open on the internet are accessed and exploited.
#
# When protected mode is on and if:
#
# 1) The server is not binding explicitly to a set of addresses using the
#    "bind" directive.
# 2) No password is configured.
#
# The server only accepts connections from clients connecting from the
# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain
# sockets.
#
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode yes

# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
port 6379

# TCP listen() backlog.
#
# In high requests-per-second environments you need an high backlog in order
# to avoid slow clients connections issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
# in order to get the desired effect.
tcp-backlog 511

# Unix socket.
#
# Specify the path for the Unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
# unixsocket /tmp/redis.sock
# unixsocketperm 700

# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0

# TCP keepalive.
#
# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
# of communication. This is useful for two reasons:
#
# 1) Detect dead peers.
# 2) Take the connection alive from the point of view of network
#    equipment in the middle.
#
# On Linux, the specified value (in seconds) is the period used to send ACKs.
# Note that to close the connection the double of the time is needed.
# On other kernels the period depends on the kernel configuration.
#
# A reasonable value for this option is 300 seconds, which is the new
# Redis default starting with Redis 3.2.1.
tcp-keepalive 300

################################# GENERAL #####################################

# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize no

# If you run Redis from upstart or systemd, Redis can interact with your
# supervision tree. Options:
#   supervised no      - no supervision interaction
#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode
#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
#   supervised auto    - detect upstart or systemd method based on
#                        UPSTART_JOB or NOTIFY_SOCKET environment variables
# Note: these supervision methods only signal "process is ready."
#       They do not enable continuous liveness pings back to your supervisor.
supervised no

# If a pid file is specified, Redis writes it where specified at startup
# and removes it at exit.
#
# When the server runs non daemonized, no pid file is created if none is
# specified in the configuration. When the server is daemonized, the pid file
# is used even if not specified, defaulting to "/var/run/redis.pid".
#
# Creating a pid file is best effort: if Redis is not able to create it
# nothing bad happens, the server will start and run normally.
pidfile /var/run/redis_6379.pid

# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
loglevel notice

# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile /var/log/redis/redis.log

# To enable logging to the system logger, just set 'syslog-enabled' to yes,
# and optionally update the other syslog parameters to suit your needs.
# syslog-enabled no

# Specify the syslog identity.
# syslog-ident redis

# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
# syslog-facility local0

# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
databases 16

################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1
save 300 10
save 60 10000

# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
#
# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes

# Compress string objects using LZF when dump .rdb databases?
# For default that's set to 'yes' as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes

# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes

# The filename where to dump the DB
dbfilename dump.rdb

# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir /var/lib/redis

################################# REPLICATION #################################

# Master-Slave replication. Use slaveof to make a Redis instance a copy of
# another Redis server. A few things to understand ASAP about Redis replication.
#
# 1) Redis replication is asynchronous, but you can configure a master to
#    stop accepting writes if it appears to be not connected with at least
#    a given number of slaves.
# 2) Redis slaves are able to perform a partial resynchronization with the
#    master if the replication link is lost for a relatively small amount of
#    time. You may want to configure the replication backlog size (see the next
#    sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
#    network partition slaves automatically try to reconnect to masters
#    and resynchronize with them.
#
# slaveof <masterip> <masterport>

# If the master is password protected (using the "requirepass" configuration
# directive below) it is possible to tell the slave to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the slave request.
#
# masterauth <master-password>

# When a slave loses its connection with the master, or when the replication
# is still in progress, the slave can act in two different ways:
#
# 1) if slave-serve-stale-data is set to 'yes' (the default) the slave will
#    still reply to client requests, possibly with out of date data, or the
#    data set may just be empty if this is the first synchronization.
#
# 2) if slave-serve-stale-data is set to 'no' the slave will reply with
#    an error "SYNC with master in progress" to all the kind of commands
#    but to INFO and SLAVEOF.
#
slave-serve-stale-data yes

# You can configure a slave instance to accept writes or not. Writing against
# a slave instance may be useful to store some ephemeral data (because data
# written on a slave will be easily deleted after resync with the master) but
# may also cause problems if clients are writing to it because of a
# misconfiguration.
#
# Since Redis 2.6 by default slaves are read-only.
#
# Note: read only slaves are not designed to be exposed to untrusted clients
# on the internet. It's just a protection layer against misuse of the instance.
# Still a read only slave exports by default all the administrative commands
# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve
# security of read only slaves using 'rename-command' to shadow all the
# administrative / dangerous commands.
slave-read-only yes

# Replication SYNC strategy: disk or socket.
#
# -------------------------------------------------------
# WARNING: DISKLESS REPLICATION IS EXPERIMENTAL CURRENTLY
# -------------------------------------------------------
#
# New slaves and reconnecting slaves that are not able to continue the replication
# process just receiving differences, need to do what is called a "full
# synchronization". An RDB file is transmitted from the master to the slaves.
# The transmission can happen in two different ways:
#
# 1) Disk-backed: The Redis master creates a new process that writes the RDB
#                 file on disk. Later the file is transferred by the parent
#                 process to the slaves incrementally.
# 2) Diskless: The Redis master creates a new process that directly writes the
#              RDB file to slave sockets, without touching the disk at all.
#
# With disk-backed replication, while the RDB file is generated, more slaves
# can be queued and served with the RDB file as soon as the current child producing
# the RDB file finishes its work. With diskless replication instead once
# the transfer starts, new slaves arriving will be queued and a new transfer
# will start when the current one terminates.
#
# When diskless replication is used, the master waits a configurable amount of
# time (in seconds) before starting the transfer in the hope that multiple slaves
# will arrive and the transfer can be parallelized.
#
# With slow disks and fast (large bandwidth) networks, diskless replication
# works better.
repl-diskless-sync no

# When diskless replication is enabled, it is possible to configure the delay
# the server waits in order to spawn the child that transfers the RDB via socket
# to the slaves.
#
# This is important since once the transfer starts, it is not possible to serve
# new slaves arriving, that will be queued for the next RDB transfer, so the server
# waits a delay in order to let more slaves arrive.
#
# The delay is specified in seconds, and by default is 5 seconds. To disable
# it entirely just set it to 0 seconds and the transfer will start ASAP.
repl-diskless-sync-delay 5

# Slaves send PINGs to server in a predefined interval. It's possible to change
# this interval with the repl_ping_slave_period option. The default value is 10
# seconds.
#
# repl-ping-slave-period 10

# The following option sets the replication timeout for:
#
# 1) Bulk transfer I/O during SYNC, from the point of view of slave.
# 2) Master timeout from the point of view of slaves (data, pings).
# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings).
#
# It is important to make sure that this value is greater than the value
# specified for repl-ping-slave-period otherwise a timeout will be detected
# every time there is low traffic between the master and the slave.
#
# repl-timeout 60

# Disable TCP_NODELAY on the slave socket after SYNC?
#
# If you select "yes" Redis will use a smaller number of TCP packets and
# less bandwidth to send data to slaves. But this can add a delay for
# the data to appear on the slave side, up to 40 milliseconds with
# Linux kernels using a default configuration.
#
# If you select "no" the delay for data to appear on the slave side will
# be reduced but more bandwidth will be used for replication.
#
# By default we optimize for low latency, but in very high traffic conditions
# or when the master and slaves are many hops away, turning this to "yes" may
# be a good idea.
repl-disable-tcp-nodelay no

# Set the replication backlog size. The backlog is a buffer that accumulates
# slave data when slaves are disconnected for some time, so that when a slave
# wants to reconnect again, often a full resync is not needed, but a partial
# resync is enough, just passing the portion of data the slave missed while
# disconnected.
#
# The bigger the replication backlog, the longer the time the slave can be
# disconnected and later be able to perform a partial resynchronization.
#
# The backlog is only allocated once there is at least a slave connected.
#
# repl-backlog-size 1mb

# After a master has no longer connected slaves for some time, the backlog
# will be freed. The following option configures the amount of seconds that
# need to elapse, starting from the time the last slave disconnected, for
# the backlog buffer to be freed.
#
# A value of 0 means to never release the backlog.
#
# repl-backlog-ttl 3600

# The slave priority is an integer number published by Redis in the INFO output.
# It is used by Redis Sentinel in order to select a slave to promote into a
# master if the master is no longer working correctly.
#
# A slave with a low priority number is considered better for promotion, so
# for instance if there are three slaves with priority 10, 100, 25 Sentinel will
# pick the one with priority 10, that is the lowest.
#
# However a special priority of 0 marks the slave as not able to perform the
# role of master, so a slave with priority of 0 will never be selected by
# Redis Sentinel for promotion.
#
# By default the priority is 100.
slave-priority 100

# It is possible for a master to stop accepting writes if there are less than
# N slaves connected, having a lag less or equal than M seconds.
#
# The N slaves need to be in "online" state.
#
# The lag in seconds, that must be <= the specified value, is calculated from
# the last ping received from the slave, that is usually sent every second.
#
# This option does not GUARANTEE that N replicas will accept the write, but
# will limit the window of exposure for lost writes in case not enough slaves
# are available, to the specified number of seconds.
#
# For example to require at least 3 slaves with a lag <= 10 seconds use:
#
# min-slaves-to-write 3
# min-slaves-max-lag 10
#
# Setting one or the other to 0 disables the feature.
#
# By default min-slaves-to-write is set to 0 (feature disabled) and
# min-slaves-max-lag is set to 10.

# A Redis master is able to list the address and port of the attached
# slaves in different ways. For example the "INFO replication" section
# offers this information, which is used, among other tools, by
# Redis Sentinel in order to discover slave instances.
# Another place where this info is available is in the output of the
# "ROLE" command of a masteer.
#
# The listed IP and address normally reported by a slave is obtained
# in the following way:
#
#   IP: The address is auto detected by checking the peer address
#   of the socket used by the slave to connect with the master.
#
#   Port: The port is communicated by the slave during the replication
#   handshake, and is normally the port that the slave is using to
#   list for connections.
#
# However when port forwarding or Network Address Translation (NAT) is
# used, the slave may be actually reachable via different IP and port
# pairs. The following two options can be used by a slave in order to
# report to its master a specific set of IP and port, so that both INFO
# and ROLE will report those values.
#
# There is no need to use both the options if you need to override just
# the port or the IP address.
#
# slave-announce-ip 5.5.5.5
# slave-announce-port 1234

################################## SECURITY ###################################

# Require clients to issue AUTH <PASSWORD> before processing any other
# commands.  This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#
# Warning: since Redis is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
#
# requirepass foobared

# Command renaming.
#
# It is possible to change the name of dangerous commands in a shared
# environment. For instance the CONFIG command may be renamed into something
# hard to guess so that it will still be available for internal-use tools
# but not available for general clients.
#
# Example:
#
# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52
#
# It is also possible to completely kill a command by renaming it into
# an empty string:
#
# rename-command CONFIG ""
#
# Please note that changing the name of commands that are logged into the
# AOF file or transmitted to slaves may cause problems.

################################### LIMITS ####################################

# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# maxclients 10000

# Don't use more memory than the specified amount of bytes.
# When the memory limit is reached Redis will try to remove keys
# according to the eviction policy selected (see maxmemory-policy).
#
# If Redis can't remove keys according to the policy, or if the policy is
# set to 'noeviction', Redis will start to reply with errors to commands
# that would use more memory, like SET, LPUSH, and so on, and will continue
# to reply to read-only commands like GET.
#
# This option is usually useful when using Redis as an LRU cache, or to set
# a hard memory limit for an instance (using the 'noeviction' policy).
#
# WARNING: If you have slaves attached to an instance with maxmemory on,
# the size of the output buffers needed to feed the slaves are subtracted
# from the used memory count, so that network problems / resyncs will
# not trigger a loop where keys are evicted, and in turn the output
# buffer of slaves is full with DELs of keys evicted triggering the deletion
# of more keys, and so forth until the database is completely emptied.
#
# In short... if you have slaves attached it is suggested that you set a lower
# limit for maxmemory so that there is some free RAM on the system for slave
# output buffers (but this is not needed if the policy is 'noeviction').
#
maxmemory 2GB

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> remove the key with an expire set using an LRU algorithm
# allkeys-lru -> remove any key according to the LRU algorithm
# volatile-random -> remove a random key with an expire set
# allkeys-random -> remove a random key, any key
# volatile-ttl -> remove the key with the nearest expire time (minor TTL)
# noeviction -> don't expire at all, just return an error on write operations
#
# Note: with any of the above policies, Redis will return an error on write
#       operations, when there are no suitable keys for eviction.
#
#       At the date of writing these commands are: set setnx setex append
#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
#       getset mset msetnx exec sort
#
# The default is:
#
maxmemory-policy allkeys-lru

# LRU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. For default Redis will check five keys and pick the one that was
# used less recently, you can change the sample size using the following
# configuration directive.
#
# The default of 5 produces good enough results. 10 Approximates very closely
# true LRU but costs a bit more CPU. 3 is very fast but not very accurate.
#
# maxmemory-samples 5

############################## APPEND ONLY MODE ###############################

# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check http://redis.io/topics/persistence for more information.

appendonly no

# The name of the append only file (default: "appendonly.aof")

appendfilename "appendonly.aof"

# The fsync() call tells the Operating System to actually write data on disk
# instead of waiting for more data in the output buffer. Some OS will really flush
# data on disk, some other OS will just try to do it ASAP.
#
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
#
# The default is "everysec", as that's usually the right compromise between
# speed and data safety. It's up to you to understand if you can relax this to
# "no" that will let the operating system flush the output buffer when
# it wants, for better performances (but if you can live with the idea of
# some data loss consider the default persistence mode that's snapshotting),
# or on the contrary, use "always" that's very slow but a bit safer than
# everysec.
#
# More details please check the following article:
# http://antirez.com/post/redis-persistence-demystified.html
#
# If unsure, use "everysec".

# appendfsync always
appendfsync everysec
# appendfsync no

# When the AOF fsync policy is set to always or everysec, and a background
# saving process (a background save or AOF log background rewriting) is
# performing a lot of I/O against the disk, in some Linux configurations
# Redis may block too long on the fsync() call. Note that there is no fix for
# this currently, as even performing fsync in a different thread will block
# our synchronous write(2) call.
#
# In order to mitigate this problem it's possible to use the following option
# that will prevent fsync() from being called in the main process while a
# BGSAVE or BGREWRITEAOF is in progress.
#
# This means that while another child is saving, the durability of Redis is
# the same as "appendfsync none". In practical terms, this means that it is
# possible to lose up to 30 seconds of log in the worst scenario (with the
# default Linux settings).
#
# If you have latency problems turn this to "yes". Otherwise leave it as
# "no" that is the safest pick from the point of view of durability.

no-appendfsync-on-rewrite no

# Automatic rewrite of the append only file.
# Redis is able to automatically rewrite the log file implicitly calling
# BGREWRITEAOF when the AOF log size grows by the specified percentage.
#
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# An AOF file may be found to be truncated at the end during the Redis
# startup process, when the AOF data gets loaded back into memory.
# This may happen when the system where Redis is running
# crashes, especially when an ext4 filesystem is mounted without the
# data=ordered option (however this can't happen when Redis itself
# crashes or aborts but the operating system still works correctly).
#
# Redis can either exit with an error when this happens, or load as much
# data as possible (the default now) and start if the AOF file is found
# to be truncated at the end. The following option controls this behavior.
#
# If aof-load-truncated is set to yes, a truncated AOF file is loaded and
# the Redis server starts emitting a log to inform the user of the event.
# Otherwise if the option is set to no, the server aborts with an error
# and refuses to start. When the option is set to no, the user requires
# to fix the AOF file using the "redis-check-aof" utility before to restart
# the server.
#
# Note that if the AOF file will be found to be corrupted in the middle
# the server will still exit with an error. This option only applies when
# Redis will try to read more data from the AOF file but not enough bytes
# will be found.
aof-load-truncated yes

################################ LUA SCRIPTING  ###############################

# Max execution time of a Lua script in milliseconds.
#
# If the maximum execution time is reached Redis will log that a script is
# still in execution after the maximum allowed time and will start to
# reply to queries with an error.
#
# When a long running script exceeds the maximum execution time only the
# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be
# used to stop a script that did not yet called write commands. The second
# is the only way to shut down the server in the case a write command was
# already issued by the script but the user doesn't want to wait for the natural
# termination of the script.
#
# Set it to 0 or a negative value for unlimited execution without warnings.
lua-time-limit 5000

################################ REDIS CLUSTER  ###############################
#
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# WARNING EXPERIMENTAL: Redis Cluster is considered to be stable code, however
# in order to mark it as "mature" we need to wait for a non trivial percentage
# of users to deploy it in production.
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
#
# Normal Redis instances can't be part of a Redis Cluster; only nodes that are
# started as cluster nodes can. In order to start a Redis instance as a
# cluster node enable the cluster support uncommenting the following:
#
# cluster-enabled yes

# Every cluster node has a cluster configuration file. This file is not
# intended to be edited by hand. It is created and updated by Redis nodes.
# Every Redis Cluster node requires a different cluster configuration file.
# Make sure that instances running in the same system do not have
# overlapping cluster configuration file names.
#
# cluster-config-file nodes-6379.conf

# Cluster node timeout is the amount of milliseconds a node must be unreachable
# for it to be considered in failure state.
# Most other internal time limits are multiple of the node timeout.
#
# cluster-node-timeout 15000

# A slave of a failing master will avoid to start a failover if its data
# looks too old.
#
# There is no simple way for a slave to actually have a exact measure of
# its "data age", so the following two checks are performed:
#
# 1) If there are multiple slaves able to failover, they exchange messages
#    in order to try to give an advantage to the slave with the best
#    replication offset (more data from the master processed).
#    Slaves will try to get their rank by offset, and apply to the start
#    of the failover a delay proportional to their rank.
#
# 2) Every single slave computes the time of the last interaction with
#    its master. This can be the last ping or command received (if the master
#    is still in the "connected" state), or the time that elapsed since the
#    disconnection with the master (if the replication link is currently down).
#    If the last interaction is too old, the slave will not try to failover
#    at all.
#
# The point "2" can be tuned by user. Specifically a slave will not perform
# the failover if, since the last interaction with the master, the time
# elapsed is greater than:
#
#   (node-timeout * slave-validity-factor) + repl-ping-slave-period
#
# So for example if node-timeout is 30 seconds, and the slave-validity-factor
# is 10, and assuming a default repl-ping-slave-period of 10 seconds, the
# slave will not try to failover if it was not able to talk with the master
# for longer than 310 seconds.
#
# A large slave-validity-factor may allow slaves with too old data to failover
# a master, while a too small value may prevent the cluster from being able to
# elect a slave at all.
#
# For maximum availability, it is possible to set the slave-validity-factor
# to a value of 0, which means, that slaves will always try to failover the
# master regardless of the last time they interacted with the master.
# (However they'll always try to apply a delay proportional to their
# offset rank).
#
# Zero is the only value able to guarantee that when all the partitions heal
# the cluster will always be able to continue.
#
# cluster-slave-validity-factor 10

# Cluster slaves are able to migrate to orphaned masters, that are masters
# that are left without working slaves. This improves the cluster ability
# to resist to failures as otherwise an orphaned master can't be failed over
# in case of failure if it has no working slaves.
#
# Slaves migrate to orphaned masters only if there are still at least a
# given number of other working slaves for their old master. This number
# is the "migration barrier". A migration barrier of 1 means that a slave
# will migrate only if there is at least 1 other working slave for its master
# and so forth. It usually reflects the number of slaves you want for every
# master in your cluster.
#
# Default is 1 (slaves migrate only if their masters remain with at least
# one slave). To disable migration just set it to a very large value.
# A value of 0 can be set but is useful only for debugging and dangerous
# in production.
#
# cluster-migration-barrier 1

# By default Redis Cluster nodes stop accepting queries if they detect there
# is at least an hash slot uncovered (no available node is serving it).
# This way if the cluster is partially down (for example a range of hash slots
# are no longer covered) all the cluster becomes, eventually, unavailable.
# It automatically returns available as soon as all the slots are covered again.
#
# However sometimes you want the subset of the cluster which is working,
# to continue to accept queries for the part of the key space that is still
# covered. In order to do so, just set the cluster-require-full-coverage
# option to no.
#
# cluster-require-full-coverage yes

# In order to setup your cluster make sure to read the documentation
# available at http://redis.io web site.

################################## SLOW LOG ###################################

# The Redis Slow Log is a system to log queries that exceeded a specified
# execution time. The execution time does not include the I/O operations
# like talking with the client, sending the reply and so forth,
# but just the time needed to actually execute the command (this is the only
# stage of command execution where the thread is blocked and can not serve
# other requests in the meantime).
#
# You can configure the slow log with two parameters: one tells Redis
# what is the execution time, in microseconds, to exceed in order for the
# command to get logged, and the other parameter is the length of the
# slow log. When a new command is logged the oldest one is removed from the
# queue of logged commands.

# The following time is expressed in microseconds, so 1000000 is equivalent
# to one second. Note that a negative number disables the slow log, while
# a value of zero forces the logging of every command.
slowlog-log-slower-than 10000

# There is no limit to this length. Just be aware that it will consume memory.
# You can reclaim memory used by the slow log with SLOWLOG RESET.
slowlog-max-len 128

################################ LATENCY MONITOR ##############################

# The Redis latency monitoring subsystem samples different operations
# at runtime in order to collect data related to possible sources of
# latency of a Redis instance.
#
# Via the LATENCY command this information is available to the user that can
# print graphs and obtain reports.
#
# The system only logs operations that were performed in a time equal or
# greater than the amount of milliseconds specified via the
# latency-monitor-threshold configuration directive. When its value is set
# to zero, the latency monitor is turned off.
#
# By default latency monitoring is disabled since it is mostly not needed
# if you don't have latency issues, and collecting data has a performance
# impact, that while very small, can be measured under big load. Latency
# monitoring can easily be enabled at runtime using the command
# "CONFIG SET latency-monitor-threshold <milliseconds>" if needed.
latency-monitor-threshold 0

############################# EVENT NOTIFICATION ##############################

# Redis can notify Pub/Sub clients about events happening in the key space.
# This feature is documented at http://redis.io/topics/notifications
#
# For instance if keyspace events notification is enabled, and a client
# performs a DEL operation on key "foo" stored in the Database 0, two
# messages will be published via Pub/Sub:
#
# PUBLISH __keyspace@0__:foo del
# PUBLISH __keyevent@0__:del foo
#
# It is possible to select the events that Redis will notify among a set
# of classes. Every class is identified by a single character:
#
#  K     Keyspace events, published with __keyspace@<db>__ prefix.
#  E     Keyevent events, published with __keyevent@<db>__ prefix.
#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
#  $     String commands
#  l     List commands
#  s     Set commands
#  h     Hash commands
#  z     Sorted set commands
#  x     Expired events (events generated every time a key expires)
#  e     Evicted events (events generated when a key is evicted for maxmemory)
#  A     Alias for g$lshzxe, so that the "AKE" string means all the events.
#
#  The "notify-keyspace-events" takes as argument a string that is composed
#  of zero or multiple characters. The empty string means that notifications
#  are disabled.
#
#  Example: to enable list and generic events, from the point of view of the
#           event name, use:
#
#  notify-keyspace-events Elg
#
#  Example 2: to get the stream of the expired keys subscribing to channel
#             name __keyevent@0__:expired use:
#
#  notify-keyspace-events Ex
#
#  By default all notifications are disabled because most users don't need
#  this feature and the feature has some overhead. Note that if you don't
#  specify at least one of K or E, no events will be delivered.
notify-keyspace-events ""

############################### ADVANCED CONFIG ###############################

# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -5: max size: 64 Kb  <-- not recommended for normal workloads
# -4: max size: 32 Kb  <-- not recommended
# -3: max size: 16 Kb  <-- probably not recommended
# -2: max size: 8 Kb   <-- good
# -1: max size: 4 Kb   <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2

# Lists may also be compressed.
# Compress depth is the number of quicklist ziplist nodes from *each* side of
# the list to *exclude* from compression.  The head and tail of the list
# are always uncompressed for fast push/pop operations.  Settings are:
# 0: disable all list compression
# 1: depth 1 means "don't start compressing until after 1 node into the list,
#    going from either the head or tail"
#    So: [head]->node->node->...->node->[tail]
#    [head], [tail] will always be uncompressed; inner nodes will compress.
# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
#    2 here means: don't compress head or head->next or tail->prev or tail,
#    but compress all nodes between them.
# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
# etc.
list-compress-depth 0

# Sets have a special encoding in just one case: when a set is composed
# of just strings that happen to be integers in radix 10 in the range
# of 64 bit signed integers.
# The following configuration setting sets the limit in the size of the
# set in order to use this special memory saving encoding.
set-max-intset-entries 512

# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

# HyperLogLog sparse representation bytes limit. The limit includes the
# 16 bytes header. When an HyperLogLog using the sparse representation crosses
# this limit, it is converted into the dense representation.
#
# A value greater than 16000 is totally useless, since at that point the
# dense representation is more memory efficient.
#
# The suggested value is ~ 3000 in order to have the benefits of
# the space efficient encoding without slowing down too much PFADD,
# which is O(N) with the sparse encoding. The value can be raised to
# ~ 10000 when CPU is not a concern, but space is, and the data set is
# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.
hll-sparse-max-bytes 3000

# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in
# order to help rehashing the main Redis hash table (the one mapping top-level
# keys to values). The hash table implementation Redis uses (see dict.c)
# performs a lazy rehashing: the more operation you run into a hash table
# that is rehashing, the more rehashing "steps" are performed, so if the
# server is idle the rehashing is never complete and some more memory is used
# by the hash table.
#
# The default is to use this millisecond 10 times every second in order to
# actively rehash the main dictionaries, freeing memory when possible.
#
# If unsure:
# use "activerehashing no" if you have hard latency requirements and it is
# not a good thing in your environment that Redis can reply from time to time
# to queries with 2 milliseconds delay.
#
# use "activerehashing yes" if you don't have such hard requirements but
# want to free memory asap when possible.
activerehashing yes

# The client output buffer limits can be used to force disconnection of clients
# that are not reading data from the server fast enough for some reason (a
# common reason is that a Pub/Sub client can't consume messages as fast as the
# publisher can produce them).
#
# The limit can be set differently for the three different classes of clients:
#
# normal -> normal clients including MONITOR clients
# slave  -> slave clients
# pubsub -> clients subscribed to at least one pubsub channel or pattern
#
# The syntax of every client-output-buffer-limit directive is the following:
#
# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
#
# A client is immediately disconnected once the hard limit is reached, or if
# the soft limit is reached and remains reached for the specified number of
# seconds (continuously).
# So for instance if the hard limit is 32 megabytes and the soft limit is
# 16 megabytes / 10 seconds, the client will get disconnected immediately
# if the size of the output buffers reach 32 megabytes, but will also get
# disconnected if the client reaches 16 megabytes and continuously overcomes
# the limit for 10 seconds.
#
# By default normal clients are not limited because they don't receive data
# without asking (in a push way), but just after a request, so only
# asynchronous clients may create a scenario where data is requested faster
# than it can read.
#
# Instead there is a default limit for pubsub and slave clients, since
# subscribers and slaves receive data in a push fashion.
#
# Both the hard or the soft limit can be disabled by setting them to zero.
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

# Redis calls an internal function to perform many background tasks, like
# closing connections of clients in timeout, purging expired keys that are
# never requested, and so forth.
#
# Not all tasks are performed with the same frequency, but Redis checks for
# tasks to perform according to the specified "hz" value.
#
# By default "hz" is set to 10. Raising the value will use more CPU when
# Redis is idle, but at the same time will make Redis more responsive when
# there are many keys expiring at the same time, and timeouts may be
# handled with more precision.
#
# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10

# When a child rewrites the AOF file, if the following option is enabled
# the file will be fsync-ed every 32 MB of data generated. This is useful
# in order to commit the file to the disk more incrementally and avoid
# big latency spikes.
aof-rewrite-incremental-fsync yes


================================================
FILE: docs/docker/spug.ini
================================================
[supervisord]
nodaemon=true

[program:nginx]
command = nginx -g "daemon off;"
autostart = true

[program:redis]
command = redis-server /etc/redis.conf
autostart = true

[program:spug-api]
command = sh /data/spug/spug_api/tools/start-api.sh
autostart = true
stdout_logfile = /data/spug/spug_api/logs/api.log
redirect_stderr = true

[program:spug-ws]
command = sh /data/spug/spug_api/tools/start-ws.sh
autostart = true
stdout_logfile = /data/spug/spug_api/logs/ws.log
redirect_stderr = true

[program:spug-worker]
command = sh /data/spug/spug_api/tools/start-worker.sh
autostart = true
stdout_logfile = /data/spug/spug_api/logs/worker.log
redirect_stderr = true

[program:spug-monitor]
command = sh /data/spug/spug_api/tools/start-monitor.sh
autostart = true
startsecs = 3
stdout_logfile = /data/spug/spug_api/logs/monitor.log
redirect_stderr = true

[program:spug-scheduler]
command = sh /data/spug/spug_api/tools/start-scheduler.sh
autostart = true
startsecs = 3
stdout_logfile = /data/spug/spug_api/logs/scheduler.log
redirect_stderr = true


================================================
FILE: docs/docker/ssh_config
================================================
Host *
  StrictHostKeyChecking no


================================================
FILE: docs/install.sh
================================================
#!/bin/bash
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.

set -e


function spug_banner() {

echo "                           ";
echo " ####  #####  #    #  #### ";
echo "#      #    # #    # #    #";
echo " ####  #    # #    # #     ";
echo "     # #####  #    # #  ###";
echo "#    # #      #    # #    #";
echo " ####  #       ####   #### ";
echo "                           ";

}


function init_system_lib() {
    source /etc/os-release
    case $ID in
        centos|fedora|rhel)
            echo "开始安装/更新可能缺少的依赖: git mariadb-server mariadb-devel python3-devel gcc openldap-devel redis nginx supervisor python36"
            yum install -y epel-release
            yum install -y git mariadb-server mariadb-devel python3-devel gcc openldap-devel redis nginx supervisor python36
            sed -i 's/ default_server//g' /etc/nginx/nginx.conf
            MYSQL_CONF=/etc/my.cnf.d/spug.cnf
            SUPERVISOR_CONF=/etc/supervisord.d/spug.ini
            REDIS_SRV=redis
            SUPERVISOR_SRV=supervisord
            ;;

        debian|ubuntu|devuan)
            echo "开始安装/更新可能缺少的依赖: git mariadb-server libmariadbd-dev python3-venv libsasl2-dev libldap2-dev redis-server nginx supervisor"
            apt update
            apt install -y git mariadb-server libmariadbd-dev python3-dev python3-venv libsasl2-dev libldap2-dev redis-server nginx supervisor
            rm -f /etc/nginx/sites-enabled/default
            MYSQL_CONF=/etc/mysql/conf.d/spug.cnf
            SUPERVISOR_CONF=/etc/supervisor/conf.d/spug.conf
            REDIS_SRV=redis-server
            SUPERVISOR_SRV=supervisor
            ;;
        *)
            exit 1
            ;;
    esac
}


function install_spug() {
  echo "开始安装Spug..."
  mkdir -p /data
  cd /data
  git clone --depth=1 https://gitee.com/openspug/spug.git
  curl -o /tmp/web_latest.tar.gz https://spug.dev/installer/web_latest.tar.gz
  tar xf /tmp/web_latest.tar.gz -C spug/spug_web/
  cd spug/spug_api
  python3 -m venv venv
  source venv/bin/activate

  pip install wheel -i https://pypi.doubanio.com/simple/
  pip install gunicorn mysqlclient -i https://pypi.doubanio.com/simple/
  pip install -r requirements.txt -i https://pypi.doubanio.com/simple/
}


function setup_conf() {

  echo "开始配置Spug配置..."
# mysql conf
cat << EOF > $MYSQL_CONF
[mysqld]
bind-address=127.0.0.1
EOF

# spug conf
cat << EOF > spug/overrides.py
DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1']

DATABASES = {
    'default': {
        'ATOMIC_REQUESTS': True,
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'spug',
        'USER': 'spug',
        'PASSWORD': 'spug.dev',
        'HOST': '127.0.0.1',
        'OPTIONS': {
            'charset': 'utf8mb4',
            'sql_mode': 'STRICT_TRANS_TABLES',
        }
    }
}
EOF

cat << EOF > $SUPERVISOR_CONF
[program:spug-api]
command = bash /data/spug/spug_api/tools/start-api.sh
autostart = true
stdout_logfile = /data/spug/spug_api/logs/api.log
redirect_stderr = true

[program:spug-ws]
command = bash /data/spug/spug_api/tools/start-ws.sh
autostart = true
stdout_logfile = /data/spug/spug_api/logs/ws.log
redirect_stderr = true

[program:spug-worker]
command = bash /data/spug/spug_api/tools/start-worker.sh
autostart = true
stdout_logfile = /data/spug/spug_api/logs/worker.log
redirect_stderr = true

[program:spug-monitor]
command = bash /data/spug/spug_api/tools/start-monitor.sh
autostart = true
stdout_logfile = /data/spug/spug_api/logs/monitor.log
redirect_stderr = true

[program:spug-scheduler]
command = bash /data/spug/spug_api/tools/start-scheduler.sh
autostart = true
stdout_logfile = /data/spug/spug_api/logs/scheduler.log
redirect_stderr = true
EOF

cat << EOF > /etc/nginx/conf.d/spug.conf
server {
        listen 80 default_server;
        root /data/spug/spug_web/build/;

        location ^~ /api/ {
                rewrite ^/api(.*) \$1 break;
                proxy_pass http://127.0.0.1:9001;
                proxy_redirect off;
                proxy_set_header X-Real-IP \$remote_addr;
        }

        location ^~ /api/ws/ {
                rewrite ^/api(.*) \$1 break;
                proxy_pass http://127.0.0.1:9002;
                proxy_http_version 1.1;
                proxy_set_header Upgrade \$http_upgrade;
                proxy_set_header Connection "Upgrade";
                proxy_set_header X-Real-IP \$remote_addr;
        }

        error_page 404 /index.html;
}
EOF


systemctl start mariadb
systemctl enable mariadb

mysql -e "create database spug default character set utf8mb4 collate utf8mb4_unicode_ci;"
mysql -e "grant all on spug.* to spug@127.0.0.1 identified by 'spug.dev'"
mysql -e "flush privileges"

python manage.py initdb
python manage.py useradd -u admin -p spug.dev -s -n 管理员


systemctl enable nginx
systemctl enable $REDIS_SRV
systemctl enable $SUPERVISOR_SRV

systemctl restart nginx
systemctl start $REDIS_SRV
systemctl restart $SUPERVISOR_SRV

}


spug_banner
init_system_lib
install_spug
setup_conf

echo -e "\n\n\033[33m安全警告:默认的数据库和Redis服务并不安全,请确保其仅监听在127.0.0.1,推荐参考官网文档自行加固安全配置!\033[0m"
echo -e "\033[32m安装成功!\033[0m"
echo "默认管理员账户:admin  密码:spug.dev"
echo "默认数据库用户:spug   密码:spug.dev"


================================================
FILE: spug_api/.gitignore
================================================
*.pyc
/venv/
__pycache__/
/.idea/
/db.sqlite3
migrations/
/access.log
/repos/*
/logs/*


================================================
FILE: spug_api/apps/account/__init__.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.


================================================
FILE: spug_api/apps/account/history.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from libs.mixins import AdminView
from libs import json_response
from apps.account.models import History


class HistoryView(AdminView):
    def get(self, request):
        histories = History.objects.all()
        return json_response(histories)


================================================
FILE: spug_api/apps/account/management/commands/set.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.core.management.base import BaseCommand
from apps.setting.utils import AppSetting


class Command(BaseCommand):
    help = '系统设置'

    def add_arguments(self, parser):
        parser.add_argument('target', type=str, help='设置对象')
        parser.add_argument('value', type=str, help='设置值')

    def echo_success(self, msg):
        self.stdout.write(self.style.SUCCESS(msg))

    def echo_error(self, msg):
        self.stderr.write(self.style.ERROR(msg))

    def print_help(self, *args):
        message = '''
        系统设置命令用法:
            set mfa disable     禁用登录MFA
        '''
        self.stdout.write(message)

    def handle(self, *args, **options):
        target = options['target']
        if target == 'mfa':
            if options['value'] != 'disable':
                return self.echo_error(f'mfa设置,不支持的值【{options["value"]}】')
            AppSetting.set('MFA', {'enable': False})
            self.echo_success('MFA已禁用')
        else:
            self.echo_error('未识别的操作')
            self.print_help()


================================================
FILE: spug_api/apps/account/management/commands/update.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.core.management.base import BaseCommand
from django.conf import settings
import subprocess
import requests
import os


class Command(BaseCommand):
    help = '升级Spug版本'

    def handle(self, *args, **options):
        version, is_repair = settings.SPUG_VERSION, False
        res = requests.get(f'https://api.spug.cc/apis/release/latest/?version={version}').json()
        if res['error']:
            return self.stderr.write(self.style.ERROR(f'获取新版本失败:{res["error"]}'))
        if not res['data']['has_new']:
            self.stdout.write(res['data']['extra'])
            is_repair = True
            answer = input(f'\r\n当前已是最新版本,是否要进行修复性更新[y|n]?')
        else:
            version = res['data']['version']
            self.stdout.write(res['data']['content'])
            self.stdout.write('\r\n')
            self.stdout.write(res['data']['extra'])
            answer = input(f'\r\n发现新版本 {version} 是否更新[y|n]?')
        if answer.lower() != 'y':
            return

        # update web
        web_dir = os.path.join(settings.BASE_DIR, '../spug_web')
        commands = [
            f'curl -o /tmp/spug_web.tar.gz https://cdn.spug.cc/spug/web_{version}.tar.gz',
            f'rm -rf {web_dir}/build',
            f'tar xf /tmp/spug_web.tar.gz -C {web_dir}'
        ]
        task = subprocess.Popen(' && '.join(commands), shell=True)
        if task.wait() != 0:
            return self.stderr.write(self.style.ERROR('获取更新失败,排除网络问题后请附带输出内容至官方论坛反馈。'))

        # update api
        commands = [
            f'cd {settings.BASE_DIR}',
            f'git fetch origin refs/tags/{version}:refs/tags/{version} --no-tags',
            f'git checkout {version}'
        ]
        if is_repair:
            commands.insert(1, f'git tag -d {version}')
        task = subprocess.Popen(' && '.join(commands), shell=True)
        if task.wait() != 0:
            return self.stderr.write(self.style.ERROR('获取更新失败,排除网络问题后请附带输出内容至官方论坛反馈。'))

        # update dep
        commands = [
            f'cd {settings.BASE_DIR}',
            'pip3 install -r requirements.txt -i https://pypi.doubanio.com/simple/'
        ]
        task = subprocess.Popen(' && '.join(commands), shell=True)
        if task.wait() != 0:
            return self.stderr.write(self.style.ERROR('更新依赖包失败,排除网络问题后请附带输出内容至官方论坛反馈。'))

        # update db
        apps = [x.split('.')[-1] for x in settings.INSTALLED_APPS if x.startswith('apps.')]
        commands = [
            f'cd {settings.BASE_DIR}',
            f'python3 ./manage.py makemigrations ' + ' '.join(apps),
            f'python3 ./manage.py migrate',
            f'python3 ./tools/migrate.py {settings.SPUG_VERSION}'
        ]
        task = subprocess.Popen(' && '.join(commands), shell=True)
        if task.wait() != 0:
            return self.stderr.write(self.style.ERROR('更新表结构失败,请附带输出内容至官方论坛反馈。'))

        self.stdout.write(self.style.SUCCESS('''升级成功,请自行重启服务,如果通过官方文档安装一般重启命令为
        Docker: docker restart spug
        Centos: systemctl restart supervisord 
        Ubuntu: systemctl restart supervisor
        '''))
        self.stderr.write(self.style.WARNING(f'最后别忘了刷新浏览器,确保系统设置/关于里的api与web版本一致哦~'))


================================================
FILE: spug_api/apps/account/management/commands/updatedb.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.core.management.base import BaseCommand
from django.core.management import execute_from_command_line
from django.conf import settings


class Command(BaseCommand):
    help = '初始化/更新数据库'

    def handle(self, *args, **options):
        args = ['manage.py', 'makemigrations']
        apps = [x.split('.')[-1] for x in settings.INSTALLED_APPS if x.startswith('apps.')]
        execute_from_command_line(args + apps)
        execute_from_command_line(['manage.py', 'migrate'])
        self.stdout.write(self.style.SUCCESS('初始化/更新成功'))


================================================
FILE: spug_api/apps/account/management/commands/user.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.core.management.base import BaseCommand
from django.core.cache import cache
from apps.account.models import User


class Command(BaseCommand):
    help = '账户管理'

    def add_arguments(self, parser):
        parser.add_argument('action', type=str, help='执行动作')
        parser.add_argument('-u', required=False, help='账户名称')
        parser.add_argument('-p', required=False, help='账户密码')
        parser.add_argument('-n', required=False, help='账户昵称')
        parser.add_argument('-s', default=False, action='store_true', help='是否是超级用户(默认否)')

    def echo_success(self, msg):
        self.stdout.write(self.style.SUCCESS(msg))

    def echo_error(self, msg):
        self.stderr.write(self.style.ERROR(msg))

    def print_help(self, *args):
        message = '''
        账户管理命令用法:
            user add    创建账户,例如:user add -u admin -p 123 -n 管理员 -s
            user reset  重置账户密码,例如:user reset -u admin -p 123
            user enable 启用被禁用的账户,例如:user enable -u admin
        '''
        self.stdout.write(message)

    def handle(self, *args, **options):
        action = options['action']
        if action == 'add':
            if not all((options['u'], options['p'], options['n'])):
                self.echo_error('缺少参数')
                self.print_help()
            elif User.objects.filter(username=options['u'], deleted_by_id__isnull=True).exists():
                self.echo_error(f'已存在登录名为【{options["u"]}】的用户')
            else:
                User.objects.create(
                    username=options['u'],
                    nickname=options['n'],
                    password_hash=User.make_password(options['p']),
                    is_supper=options['s'],
                )
                self.echo_success('创建用户成功')
        elif action == 'enable':
            if not options['u']:
                self.echo_error('缺少参数')
                self.print_help()
            user = User.objects.filter(username=options['u'], deleted_by_id__isnull=True).first()
            if user:
                user.is_active = True
                user.save()
            cache.delete(user.username)
            self.echo_success('账户已启用')
        elif action == 'reset':
            if not all((options['u'], options['p'])):
                self.echo_error('缺少参数')
                self.print_help()
            user = User.objects.filter(username=options['u'], deleted_by_id__isnull=True).first()
            if not user:
                return self.echo_error(f'未找到登录名为【{options["u"]}】的账户')
            user.password_hash = User.make_password(options['p'])
            user.save()
            cache.delete(user.username)
            self.echo_success('账户密码已重置')
        else:
            self.echo_error('未识别的操作')
            self.print_help()


================================================
FILE: spug_api/apps/account/models.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.db import models
from django.core.cache import cache
from libs import ModelMixin, human_datetime
from django.contrib.auth.hashers import make_password, check_password
import json


class User(models.Model, ModelMixin):
    username = models.CharField(max_length=100)
    nickname = models.CharField(max_length=100)
    password_hash = models.CharField(max_length=100)  # hashed password
    type = models.CharField(max_length=20, default='default')
    is_supper = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    access_token = models.CharField(max_length=32)
    token_expired = models.IntegerField(null=True)
    last_login = models.CharField(max_length=20)
    last_ip = models.CharField(max_length=50)
    wx_token = models.CharField(max_length=50, null=True)
    roles = models.ManyToManyField('Role', db_table='user_role_rel')

    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey('User', models.PROTECT, related_name='+', null=True)
    deleted_at = models.CharField(max_length=20, null=True)
    deleted_by = models.ForeignKey('User', models.PROTECT, related_name='+', null=True)

    @staticmethod
    def make_password(plain_password: str) -> str:
        return make_password(plain_password, hasher='pbkdf2_sha256')

    def verify_password(self, plain_password: str) -> bool:
        return check_password(plain_password, self.password_hash)

    def get_perms_cache(self):
        return cache.get(f'perms_{self.id}', set())

    def set_perms_cache(self, value=None):
        cache.set(f'perms_{self.id}', value or set())

    @property
    def page_perms(self):
        data = self.get_perms_cache()
        if data:
            return data
        for item in self.roles.all():
            if item.page_perms:
                perms = json.loads(item.page_perms)
                for m, v in perms.items():
                    for p, d in v.items():
                        data.update(f'{m}.{p}.{x}' for x in d)
        self.set_perms_cache(data)
        return data

    @property
    def deploy_perms(self):
        data = {'apps': set(), 'envs': set()}
        for item in self.roles.all():
            if item.deploy_perms:
                perms = json.loads(item.deploy_perms)
                data['apps'].update(perms.get('apps', []))
                data['envs'].update(perms.get('envs', []))
        data['apps'].update(x.id for x in self.app_set.all())
        return data

    @property
    def group_perms(self):
        data = set()
        for item in self.roles.all():
            if item.group_perms:
                data.update(json.loads(item.group_perms))
        return list(data)

    def has_perms(self, codes):
        if self.is_supper:
            return True
        return self.page_perms.intersection(codes)

    def __repr__(self):
        return '<User %r>' % self.username

    class Meta:
        db_table = 'users'
        ordering = ('-id',)


class Role(models.Model, ModelMixin):
    name = models.CharField(max_length=50)
    desc = models.CharField(max_length=255, null=True)
    page_perms = models.TextField(null=True)
    deploy_perms = models.TextField(null=True)
    group_perms = models.TextField(null=True)
    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='+')

    def to_dict(self, *args, **kwargs):
        tmp = super().to_dict(*args, **kwargs)
        tmp['page_perms'] = json.loads(self.page_perms) if self.page_perms else {}
        tmp['deploy_perms'] = json.loads(self.deploy_perms) if self.deploy_perms else {}
        tmp['group_perms'] = json.loads(self.group_perms) if self.group_perms else []
        tmp['used'] = self.user_set.filter(deleted_by_id__isnull=True).count()
        return tmp

    def add_deploy_perm(self, target, value):
        perms = {'apps': [], 'envs': []}
        if self.deploy_perms:
            perms.update(json.loads(self.deploy_perms))
        perms[target].append(value)
        self.deploy_perms = json.dumps(perms)
        self.save()

    def clear_perms_cache(self):
        for item in self.user_set.all():
            item.set_perms_cache()

    def __repr__(self):
        return '<Role name=%r>' % self.name

    class Meta:
        db_table = 'roles'
        ordering = ('-id',)


class History(models.Model, ModelMixin):
    username = models.CharField(max_length=100, null=True)
    type = models.CharField(max_length=20, default='default')
    ip = models.CharField(max_length=50)
    agent = models.CharField(max_length=255, null=True)
    message = models.CharField(max_length=255, null=True)
    is_success = models.BooleanField(default=True)
    created_at = models.CharField(max_length=20, default=human_datetime)

    class Meta:
        db_table = 'login_histories'
        ordering = ('-id',)


================================================
FILE: spug_api/apps/account/urls.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.conf.urls import url

from apps.account.views import *
from apps.account.history import *

urlpatterns = [
    url(r'^login/$', login),
    url(r'^logout/$', logout),
    url(r'^user/$', UserView.as_view()),
    url(r'^role/$', RoleView.as_view()),
    url(r'^self/$', SelfView.as_view()),
    url(r'^login/history/$', HistoryView.as_view())
]


================================================
FILE: spug_api/apps/account/utils.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from apps.host.models import Group
import re


def get_host_perms(user):
    ids = sub_ids = set(user.group_perms)
    while sub_ids:
        sub_ids = [x.id for x in Group.objects.filter(parent_id__in=sub_ids)]
        ids.update(sub_ids)
    return set(x.host_id for x in Group.hosts.through.objects.filter(group_id__in=ids))


def has_host_perm(user, target):
    if user.is_supper:
        return True
    host_ids = get_host_perms(user)
    if isinstance(target, (list, set, tuple)):
        return set(target).issubset(host_ids)
    return int(target) in host_ids


def verify_password(password):
    if len(password) < 8:
        return False
    if not all(map(lambda x: re.findall(x, password), ['[0-9]', '[a-z]', '[A-Z]'])):
        return False
    return True


================================================
FILE: spug_api/apps/account/views.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.core.cache import cache
from django.conf import settings
from libs.mixins import AdminView, View
from libs import JsonParser, Argument, human_datetime, json_response
from libs.utils import get_request_real_ip, generate_random_str
from libs.push import send_login_code
from apps.account.models import User, Role, History
from apps.setting.utils import AppSetting
from apps.account.utils import verify_password
from libs.ldap import LDAP
from functools import partial
import user_agents
import ipaddress
import time
import uuid
import json


class UserView(AdminView):
    def get(self, request):
        users = []
        for u in User.objects.filter(deleted_by_id__isnull=True):
            tmp = u.to_dict(excludes=('access_token', 'password_hash'))
            tmp['role_ids'] = [x.id for x in u.roles.all()]
            tmp['password'] = '******'
            users.append(tmp)
        return json_response(users)

    def post(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False),
            Argument('username', help='请输入登录名'),
            Argument('password', help='请输入密码'),
            Argument('nickname', help='请输入姓名'),
            Argument('role_ids', type=list, default=[]),
            Argument('wx_token', required=False),
        ).parse(request.body)
        if error is None:
            user = User.objects.filter(username=form.username, deleted_by_id__isnull=True).first()
            if user and (not form.id or form.id != user.id):
                return json_response(error=f'已存在登录名为【{form.username}】的用户')

            role_ids, password = form.pop('role_ids'), form.pop('password')
            if form.id:
                user = User.objects.get(pk=form.id)
                user.update_by_dict(form)
            else:
                if not verify_password(password):
                    return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')
                user = User.objects.create(
                    password_hash=User.make_password(password),
                    created_by=request.user,
                    **form
                )
            user.roles.set(role_ids)
            user.set_perms_cache()
        return json_response(error=error)

    def patch(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='参数错误'),
            Argument('password', required=False),
            Argument('is_active', type=bool, required=False),
        ).parse(request.body)
        if error is None:
            user = User.objects.get(pk=form.id)
            if form.password:
                if not verify_password(form.password):
                    return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')
                user.token_expired = 0
                user.password_hash = User.make_password(form.pop('password'))
            if form.is_active is not None:
                user.is_active = form.is_active
                cache.delete(user.username)
            user.save()
        return json_response(error=error)

    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='请指定操作对象')
        ).parse(request.GET)
        if error is None:
            user = User.objects.filter(pk=form.id).first()
            if user:
                if user.type == 'ldap':
                    return json_response(error='ldap账户无法删除,请使用禁用功能来禁止该账户访问系统')
                if user.id == request.user.id:
                    return json_response(error='无法删除当前登录账户')
                user.is_active = True
                user.deleted_at = human_datetime()
                user.deleted_by = request.user
                user.roles.clear()
                user.save()
        return json_response(error=error)


class RoleView(AdminView):
    def get(self, request):
        roles = Role.objects.all()
        return json_response(roles)

    def post(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False),
            Argument('name', help='请输入角色名称'),
            Argument('desc', required=False)
        ).parse(request.body)
        if error is None:
            if form.id:
                Role.objects.filter(pk=form.id).update(**form)
            else:
                Role.objects.create(created_by=request.user, **form)
        return json_response(error=error)

    def patch(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='参数错误'),
            Argument('page_perms', type=dict, required=False),
            Argument('deploy_perms', type=dict, required=False),
            Argument('group_perms', type=list, required=False)
        ).parse(request.body)
        if error is None:
            role = Role.objects.filter(pk=form.pop('id')).first()
            if not role:
                return json_response(error='未找到指定角色')
            if form.page_perms is not None:
                role.page_perms = json.dumps(form.page_perms)
                role.clear_perms_cache()
            if form.deploy_perms is not None:
                role.deploy_perms = json.dumps(form.deploy_perms)
            if form.group_perms is not None:
                role.group_perms = json.dumps(form.group_perms)
            role.user_set.update(token_expired=0)
            role.save()
        return json_response(error=error)

    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='参数错误')
        ).parse(request.GET)
        if error is None:
            role = Role.objects.get(pk=form.id)
            if role.user_set.exists():
                return json_response(error='已有用户使用了该角色,请解除关联后再尝试删除')
            role.delete()
        return json_response(error=error)


class SelfView(View):
    def get(self, request):
        data = request.user.to_dict(selects=('nickname', 'wx_token'))
        return json_response(data)

    def patch(self, request):
        form, error = JsonParser(
            Argument('old_password', required=False),
            Argument('new_password', required=False),
            Argument('nickname', required=False, help='请输入昵称'),
            Argument('wx_token', required=False),
        ).parse(request.body)
        if error is None:
            if form.old_password and form.new_password:
                if request.user.type == 'ldap':
                    return json_response(error='LDAP账户无法修改密码')

                if not verify_password(form.new_password):
                    return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')

                if request.user.verify_password(form.old_password):
                    request.user.password_hash = User.make_password(form.new_password)
                    request.user.token_expired = 0
                    request.user.save()
                    return json_response()
                else:
                    return json_response(error='原密码错误,请重新输入')
            if form.nickname is not None:
                request.user.nickname = form.nickname
            if form.wx_token is not None:
                request.user.wx_token = form.wx_token
            request.user.save()
        return json_response(error=error)


def login(request):
    form, error = JsonParser(
        Argument('username', help='请输入用户名'),
        Argument('password', help='请输入密码'),
        Argument('captcha', required=False),
        Argument('type', required=False)
    ).parse(request.body)
    if error is None:
        handle_response = partial(handle_login_record, request, form.username, form.type)
        user = User.objects.filter(username=form.username, type=form.type).first()
        if user and not user.is_active:
            return handle_response(error="账户已被系统禁用")
        if form.type == 'ldap':
            config = AppSetting.get_default('ldap_service')
            if not config:
                return handle_response(error='请在系统设置中配置LDAP后再尝试通过该方式登录')
            ldap = LDAP(**config)
            is_success, message = ldap.valid_user(form.username, form.password)
            if is_success:
                if not user:
                    user = User.objects.create(username=form.username, nickname=form.username, type=form.type)
                return handle_user_info(handle_response, request, user, form.captcha)
            elif message:
                return handle_response(error=message)
        else:
            if user and user.deleted_by is None:
                if user.verify_password(form.password):
                    return handle_user_info(handle_response, request, user, form.captcha)

        value = cache.get_or_set(form.username, 0, 86400)
        if value >= 3:
            if user and user.is_active:
                user.is_active = False
                user.save()
            return handle_response(error='账户已被系统禁用')
        cache.set(form.username, value + 1, 86400)
        return handle_response(error="用户名或密码错误,连续多次错误账户将会被禁用")
    return json_response(error=error)


def handle_login_record(request, username, login_type, error=None):
    x_real_ip = get_request_real_ip(request.headers)
    user_agent = user_agents.parse(request.headers.get('User-Agent'))
    History.objects.create(
        username=username,
        type=login_type,
        ip=x_real_ip,
        agent=user_agent,
        is_success=False if error else True,
        message=error
    )
    if error:
        return json_response(error=error)


def handle_user_info(handle_response, request, user, captcha):
    cache.delete(user.username)
    key = f'{user.username}:code'
    if captcha:
        code = cache.get(key)
        if not code:
            return handle_response(error='验证码已失效,请重新获取')
        if code != captcha:
            ttl = cache.ttl(key)
            cache.expire(key, ttl - 100)
            return handle_response(error='验证码错误')
        cache.delete(key)
    else:
        mfa = AppSetting.get_default('MFA', {'enable': False})
        if mfa['enable']:
            if not user.wx_token:
                return handle_response(error='已启用登录双重认证,但您的账户未配置推送标识,请联系管理员')
            spug_push_key = AppSetting.get_default('spug_push_key')
            if not spug_push_key:
                return handle_response(error='已启用登录双重认证,但系统未配置推送服务,请联系管理员')
            code = generate_random_str(6)
            send_login_code(spug_push_key, user.wx_token, code)
            cache.set(key, code, 300)
            return json_response({'required_mfa': True})

    handle_response()
    x_real_ip = get_request_real_ip(request.headers)
    token_isvalid = user.access_token and len(user.access_token) == 32 and user.token_expired >= time.time()
    user.access_token = user.access_token if token_isvalid else uuid.uuid4().hex
    user.token_expired = time.time() + settings.TOKEN_TTL
    user.last_login = human_datetime()
    user.last_ip = x_real_ip
    user.save()
    verify_ip = AppSetting.get_default('verify_ip', True)
    return json_response({
        'id': user.id,
        'access_token': user.access_token,
        'nickname': user.nickname,
        'is_supper': user.is_supper,
        'has_real_ip': x_real_ip and ipaddress.ip_address(x_real_ip).is_global if verify_ip else True,
        'permissions': [] if user.is_supper else list(user.page_perms)
    })


def logout(request):
    request.user.token_expired = 0
    request.user.save()
    return json_response()


================================================
FILE: spug_api/apps/alarm/__init__.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.


================================================
FILE: spug_api/apps/alarm/models.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.db import models
from libs import ModelMixin, human_datetime
from apps.account.models import User
import json


class Alarm(models.Model, ModelMixin):
    MODES = (
        ('1', '微信'),
        ('2', '短信'),
        ('3', '钉钉'),
        ('4', '邮件'),
        ('5', '企业微信'),
        ('6', '电话'),
        ('7', '飞书'),
    )
    STATUS = (
        ('1', '报警发生'),
        ('2', '故障恢复'),
    )
    name = models.CharField(max_length=50)
    type = models.CharField(max_length=50)
    target = models.CharField(max_length=100)
    notify_mode = models.CharField(max_length=255)
    notify_grp = models.CharField(max_length=255)
    status = models.CharField(max_length=2, choices=STATUS)
    duration = models.CharField(max_length=50)
    created_at = models.CharField(max_length=20, default=human_datetime)

    def to_dict(self, *args, **kwargs):
        tmp = super().to_dict(*args, **kwargs)
        tmp['notify_mode'] = ','.join(dict(self.MODES)[x] for x in json.loads(self.notify_mode))
        tmp['notify_grp'] = json.loads(self.notify_grp)
        tmp['status_alias'] = self.get_status_display()
        return tmp

    def __repr__(self):
        return '<Alarm %r>' % self.name

    class Meta:
        db_table = 'alarms'
        ordering = ('-id',)


class Group(models.Model, ModelMixin):
    name = models.CharField(max_length=50)
    desc = models.CharField(max_length=255, null=True)
    contacts = models.TextField(null=True)
    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey(User, models.PROTECT, related_name='+')

    def to_dict(self, *args, **kwargs):
        tmp = super().to_dict(*args, **kwargs)
        tmp['contacts'] = json.loads(self.contacts)
        return tmp

    def __repr__(self):
        return '<AlarmGroup %r>' % self.name

    class Meta:
        db_table = 'alarm_groups'
        ordering = ('-id',)


class Contact(models.Model, ModelMixin):
    name = models.CharField(max_length=50)
    phone = models.CharField(max_length=20, null=True)
    email = models.CharField(max_length=255, null=True)
    ding = models.CharField(max_length=255, null=True)
    wx_token = models.CharField(max_length=255, null=True)
    qy_wx = models.CharField(max_length=255, null=True)
    feishu = models.CharField(max_length=255, null=True)
    secret = models.TextField(null=True)

    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey(User, models.PROTECT, related_name='+')

    def __repr__(self):
        return '<AlarmContact %r>' % self.name

    class Meta:
        db_table = 'alarm_contacts'
        ordering = ('-id',)


================================================
FILE: spug_api/apps/alarm/urls.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.urls import path

from .views import *

urlpatterns = [
    path('alarm/', AlarmView.as_view()),
    path('group/', GroupView.as_view()),
    path('contact/', ContactView.as_view()),
    path('test/', handle_test),
]


================================================
FILE: spug_api/apps/alarm/views.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.views.generic import View
from libs import json_response, JsonParser, Argument, auth
from libs.spug import Notification
from libs.push import get_contacts
from apps.alarm.models import Alarm, Group, Contact
from apps.monitor.models import Detection
from apps.setting.utils import AppSetting
import json


class AlarmView(View):
    @auth('alarm.alarm.view')
    def get(self, request):
        alarms = Alarm.objects.all()
        return json_response(alarms)


class GroupView(View):
    @auth('alarm.group.view|monitor.monitor.add|monitor.monitor.edit|alarm.alarm.view')
    def get(self, request):
        groups = Group.objects.all()
        return json_response(groups)

    @auth('alarm.group.add|alarm.group.edit')
    def post(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False),
            Argument('name', help='请输入组名'),
            Argument('contacts', type=list, help='请选择联系人'),
            Argument('desc', required=False)
        ).parse(request.body)
        if error is None:
            form.contacts = json.dumps(form.contacts)
            if form.id:
                Group.objects.filter(pk=form.id).update(**form)
            else:
                form.created_by = request.user
                Group.objects.create(**form)
        return json_response(error=error)

    @auth('alarm.group.del')
    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='请指定操作对象')
        ).parse(request.GET)
        if error is None:
            detection = Detection.objects.filter(notify_grp__regex=fr'[^0-9]{form.id}[^0-9]').first()
            if detection:
                return json_response(error=f'监控任务【{detection.name}】正在使用该报警组,请解除关联后再尝试删除该联系组')
            Group.objects.filter(pk=form.id).delete()
        return json_response(error=error)


class ContactView(View):
    @auth('alarm.contact.view|alarm.group.view|schedule.schedule.add|schedule.schedule.edit')
    def get(self, request):
        form, error = JsonParser(
            Argument('with_push', required=False),
            Argument('only_push', required=False),
        ).parse(request.GET)
        if error is None:
            response = []
            if form.with_push or form.only_push:
                push_key = AppSetting.get_default('spug_push_key')
                if push_key:
                    response = get_contacts(push_key)
                if form.only_push:
                    return json_response(response)

            for item in Contact.objects.all():
                response.append(item.to_dict())
            return json_response(response)
        return json_response(error=error)

    @auth('alarm.contact.add|alarm.contact.edit')
    def post(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False),
            Argument('name', help='请输入联系人姓名'),
            Argument('phone', required=False),
            Argument('email', required=False),
            Argument('ding', required=False),
            Argument('wx_token', required=False),
            Argument('qy_wx', required=False),
            Argument('feishu', required=False),
            Argument('secret', required=False),
        ).parse(request.body)
        if error is None:
            if form.id:
                Contact.objects.filter(pk=form.id).update(**form)
            else:
                form.created_by = request.user
                Contact.objects.create(**form)
        return json_response(error=error)

    @auth('alarm.contact.del')
    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='请指定操作对象')
        ).parse(request.GET)
        if error is None:
            group = Group.objects.filter(contacts__contains=f'\"{form.id}\"').first()
            if group:
                return json_response(error=f'报警联系组【{group.name}】包含此联系人,请解除关联后再尝试删除该联系人')
            Contact.objects.filter(pk=form.id).delete()
        return json_response(error=error)


@auth('alarm.contact.add|alarm.contact.edit')
def handle_test(request):
    form, error = JsonParser(
        Argument('mode', help='参数错误'),
        Argument('value', help='参数错误')
    ).parse(request.body)
    if error is None:
        notify = Notification(None, '1', 'https://spug.cc', 'Spug官网(测试)', '这是一条测试告警信息', None)
        if form.mode == '3':
            notify.monitor_by_dd([(form.value, None)])
        elif form.mode == '4':
            notify.monitor_by_email([form.value])
        elif form.mode == '5':
            notify.monitor_by_qy_wx([form.value])
        elif form.mode == '7':
            notify.monitor_by_fs([(form.value, None)])
    return json_response(error=error)


================================================
FILE: spug_api/apps/apis/__init__.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.


================================================
FILE: spug_api/apps/apis/config.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.http.response import HttpResponse
from django_redis import get_redis_connection
from apps.config.models import Environment
from apps.app.models import App
from apps.setting.utils import AppSetting
from apps.config.utils import compose_configs
import json


def get_configs(request):
    app, env_id, no_prefix = _parse_params(request)
    if not app or not env_id:
        return HttpResponse('Invalid params', status=400)

    configs = compose_configs(app, env_id, no_prefix)
    fmt = request.GET.get('format', 'kv')
    if fmt == 'kv':
        return _kv_response(configs)
    elif fmt == 'env':
        return _env_response(configs)
    elif fmt == 'json':
        return _json_response(configs)
    else:
        return HttpResponse('Unsupported output format', status=400)


def _kv_response(data):
    output = ''
    for k, v in sorted(data.items()):
        output += f'{k} = {v}\r\n'
    return HttpResponse(output, content_type='text/plain; charset=utf-8')


def _env_response(data):
    output = ''
    for k, v in sorted(data.items()):
        output += f'{k}={v}\n'
    return HttpResponse(output, content_type='text/plain; charset=utf-8')


def _json_response(data):
    data = dict(sorted(data.items()))
    return HttpResponse(json.dumps(data), content_type='application/json')


def _parse_params(request):
    app, env_id = None, None
    api_token = request.GET.get('apiToken')
    if api_token:
        rds = get_redis_connection()
        content = rds.get(api_token)
        if content:
            app_id, env_id = content.decode().split(',')
            app = App.objects.filter(pk=app_id).first()
    else:
        api_key = AppSetting.get_default('api_key')
        if api_key and request.GET.get('apiKey') == api_key:
            app_key = request.GET.get('app')
            env_key = request.GET.get('env')
            if app_key and env_key:
                app = App.objects.filter(key=app_key).first()
                env = Environment.objects.filter(key=env_key).first()
                if env:
                    env_id = env.id
    return app, env_id, request.GET.get('noPrefix')


================================================
FILE: spug_api/apps/apis/deploy.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.http.response import HttpResponseBadRequest, HttpResponseForbidden, HttpResponse
from apps.setting.utils import AppSetting
from apps.deploy.models import Deploy, DeployRequest
from apps.repository.models import Repository
from apps.deploy.utils import dispatch as deploy_dispatch
from libs.utils import human_datetime
from threading import Thread
import hashlib
import hmac
import json


def auto_deploy(request, deploy_id, kind):
    repo, body = _parse_request(request)
    if not repo:
        return HttpResponseForbidden()

    try:
        _, _kind, ref = body['ref'].split('/', 2)
        if kind == 'branch' and _kind == 'heads':
            commit_id = body['after']
            if commit_id != '0000000000000000000000000000000000000000' and ref == request.GET.get('name'):
                message = _parse_message(body, repo)
                Thread(target=_dispatch, args=(deploy_id, ref, commit_id, message)).start()
                return HttpResponse(status=202)
        elif kind == 'tag' and _kind == 'tags':
            Thread(target=_dispatch, args=(deploy_id, ref)).start()
            return HttpResponse(status=202)
        return HttpResponse(status=204)
    except Exception as e:
        return HttpResponseBadRequest(e)


def _parse_request(request):
    api_key = AppSetting.get_default('api_key')
    token, repo, body = None, None, None
    token = request.headers.get('X-Gitlab-Token')
    if 'X-Gitlab-Token' in request.headers:
        token = request.headers['X-Gitlab-Token']
        repo = 'Gitlab'
    elif 'X-Gitee-Token' in request.headers:
        token = request.headers['X-Gitee-Token']
        repo = 'Gitee'
    elif 'X-Codeup-Token' in request.headers:
        token = request.headers['X-Codeup-Token']
        repo = 'Codeup'
    elif 'X-Gogs-Signature' in request.headers:
        token = request.headers['X-Gogs-Signature']
        repo = 'Gogs'
    elif 'X-Hub-Signature-256' in request.headers:
        token = request.headers['X-Hub-Signature-256'].replace('sha256=', '')
        repo = 'Github'
    elif 'X-Coding-Signature' in request.headers:
        token = request.headers['X-Coding-Signature'].replace('sha1=', '')
        repo = 'Coding'
    elif 'token' in request.GET:  # Compatible the old version of gitlab
        token = request.GET.get('token')
        repo = 'Gitlab'

    if repo in ['Gitlab', 'Gitee', 'Codeup']:
        if token != api_key:
            return None, None
    elif repo in ['Github', 'Gogs']:
        en_api_key = hmac.new(api_key.encode(), request.body, hashlib.sha256).hexdigest()
        if token != en_api_key:
            return None, None
    elif repo in ['Coding']:
        en_api_key = hmac.new(api_key.encode(), request.body, hashlib.sha1).hexdigest()
        if token != en_api_key:
            return None, None
    else:
        return None, None

    body = json.loads(request.body)
    if repo == 'Gogs' and not body['ref'].startswith('refs/'):
        body['ref'] = 'refs/tags/' + body['ref']

    return repo, body


def _parse_message(body, repo):
    message = None
    if repo in ['Gitee', 'Github', 'Coding']:
        message = body.get('head_commit', {}).get('message', '')
    elif repo in ['Gitlab', 'Codeup', 'Gogs']:
        if body.get('commits'):
            message = body['commits'][0].get('message', '')
    else:
        raise ValueError(f'repo {repo} is not supported')
    return message[:20].strip()


def _dispatch(deploy_id, ref, commit_id=None, message=None):
    deploy = Deploy.objects.filter(pk=deploy_id).first()
    if not deploy:
        raise Exception(f'no such deploy id for {deploy_id}')

    req = DeployRequest(
        type='3',
        status='0' if deploy.is_audit else '2',
        deploy=deploy,
        spug_version=Repository.make_spug_version(deploy.id),
        host_ids=deploy.host_ids,
        created_by=deploy.created_by
    )

    if commit_id:  # branch
        req.version = f'{ref}#{commit_id[:6]}'
        req.name = message or req.version
        if deploy.extend == '1':
            req.extra = json.dumps(['branch', ref, commit_id])
    else:  # tag
        req.version = ref
        req.name = ref
        if deploy.extend == '1':
            req.extra = json.dumps(['tag', ref, None])

    req.save()
    if req.status == '2':
        req.do_at = human_datetime()
        req.do_by = deploy.created_by
        req.save()
        deploy_dispatch(req)


================================================
FILE: spug_api/apps/apis/urls.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.urls import path

from apps.apis import config
from apps.apis import deploy

urlpatterns = [
    path('config/', config.get_configs),
    path('deploy/<int:deploy_id>/<str:kind>/', deploy.auto_deploy)
]


================================================
FILE: spug_api/apps/app/__init__.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.


================================================
FILE: spug_api/apps/app/models.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.db import models
from django.conf import settings
from libs import ModelMixin, human_datetime
from apps.account.models import User
from apps.config.models import Environment
import subprocess
import json
import os


class App(models.Model, ModelMixin):
    name = models.CharField(max_length=50)
    key = models.CharField(max_length=50, unique=True)
    desc = models.CharField(max_length=255, null=True)
    rel_apps = models.TextField(null=True)
    rel_services = models.TextField(null=True)
    sort_id = models.IntegerField(default=0, db_index=True)
    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey(User, on_delete=models.PROTECT)

    def to_dict(self, *args, **kwargs):
        tmp = super().to_dict(*args, **kwargs)
        tmp['rel_apps'] = json.loads(self.rel_apps) if self.rel_apps else []
        tmp['rel_services'] = json.loads(self.rel_services) if self.rel_services else []
        return tmp

    def __repr__(self):
        return f'<App {self.name!r}>'

    class Meta:
        db_table = 'apps'
        ordering = ('-sort_id',)


class Deploy(models.Model, ModelMixin):
    EXTENDS = (
        ('1', '常规发布'),
        ('2', '自定义发布'),
    )
    app = models.ForeignKey(App, on_delete=models.PROTECT)
    env = models.ForeignKey(Environment, on_delete=models.PROTECT)
    host_ids = models.TextField()
    extend = models.CharField(max_length=2, choices=EXTENDS)
    is_audit = models.BooleanField()
    is_parallel = models.BooleanField(default=True)
    rst_notify = models.CharField(max_length=255, null=True)
    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey(User, models.PROTECT, related_name='+')
    updated_at = models.CharField(max_length=20, null=True)
    updated_by = models.ForeignKey(User, models.PROTECT, related_name='+', null=True)

    @property
    def extend_obj(self):
        cls = DeployExtend1 if self.extend == '1' else DeployExtend2
        return cls.objects.filter(deploy=self).first()

    def to_dict(self, *args, **kwargs):
        deploy = super().to_dict(*args, **kwargs)
        deploy['app_key'] = self.app_key if hasattr(self, 'app_key') else None
        deploy['app_name'] = self.app_name if hasattr(self, 'app_name') else None
        deploy['host_ids'] = json.loads(self.host_ids)
        deploy['rst_notify'] = json.loads(self.rst_notify)
        deploy.update(self.extend_obj.to_dict())
        return deploy

    def delete(self, using=None, keep_parents=False):
        deploy_id = self.id
        super().delete(using, keep_parents)
        repo_dir = os.path.join(settings.REPOS_DIR, str(deploy_id))
        build_dir = os.path.join(settings.BUILD_DIR, f'{deploy_id}_*')
        subprocess.Popen(f'rm -rf {repo_dir} {repo_dir + "_*"} {build_dir}', shell=True)

    def __repr__(self):
        return '<Deploy app_id=%r env_id=%r>' % (self.app_id, self.env_id)

    class Meta:
        db_table = 'deploys'
        ordering = ('-id',)


class DeployExtend1(models.Model, ModelMixin):
    deploy = models.OneToOneField(Deploy, primary_key=True, on_delete=models.CASCADE)
    git_repo = models.CharField(max_length=255)
    dst_dir = models.CharField(max_length=255)
    dst_repo = models.CharField(max_length=255)
    versions = models.IntegerField()
    filter_rule = models.TextField()
    hook_pre_server = models.TextField(null=True)
    hook_post_server = models.TextField(null=True)
    hook_pre_host = models.TextField(null=True)
    hook_post_host = models.TextField(null=True)

    def to_dict(self, *args, **kwargs):
        tmp = super().to_dict(*args, **kwargs)
        tmp['filter_rule'] = json.loads(self.filter_rule)
        return tmp

    def __repr__(self):
        return '<DeployExtend1 deploy_id=%r>' % self.deploy_id

    class Meta:
        db_table = 'deploy_extend1'


class DeployExtend2(models.Model, ModelMixin):
    deploy = models.OneToOneField(Deploy, primary_key=True, on_delete=models.CASCADE)
    server_actions = models.TextField()
    host_actions = models.TextField()
    require_upload = models.BooleanField(default=False)

    def to_dict(self, *args, **kwargs):
        tmp = super().to_dict(*args, **kwargs)
        tmp['server_actions'] = json.loads(self.server_actions)
        tmp['host_actions'] = json.loads(self.host_actions)
        return tmp

    def __repr__(self):
        return '<DeployExtend2 deploy_id=%r>' % self.deploy_id

    class Meta:
        db_table = 'deploy_extend2'


================================================
FILE: spug_api/apps/app/urls.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.urls import path

from .views import *

urlpatterns = [
    path('', AppView.as_view()),
    path('kit/key/', kit_key),
    path('deploy/', DeployView.as_view()),
    path('deploy/<int:d_id>/versions/', get_versions),
]


================================================
FILE: spug_api/apps/app/utils.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.conf import settings
from apps.app.models import Deploy
from apps.setting.utils import AppSetting
from libs.gitlib import Git
import shutil
import os


def parse_envs(text):
    data = {}
    if text:
        for line in text.split('\n'):
            fields = line.split('=', 1)
            if len(fields) != 2 or fields[0].strip() == '':
                raise Exception(f'解析自定义全局变量{line!r}失败,确认其遵循 key = value 格式')
            data[fields[0].strip()] = fields[1].strip()
    return data


def fetch_versions(deploy: Deploy):
    git_repo = deploy.extend_obj.git_repo
    repo_dir = os.path.join(settings.REPOS_DIR, str(deploy.id))
    pkey = AppSetting.get_default('private_key')
    with Git(git_repo, repo_dir, pkey) as git:
        return git.fetch_branches_tags()


def fetch_repo(deploy_id, git_repo):
    repo_dir = os.path.join(settings.REPOS_DIR, str(deploy_id))
    pkey = AppSetting.get_default('private_key')
    with Git(git_repo, repo_dir, pkey) as git:
        return git.fetch_branches_tags()


def remove_repo(deploy_id):
    shutil.rmtree(os.path.join(settings.REPOS_DIR, str(deploy_id)), True)


================================================
FILE: spug_api/apps/app/views.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.views.generic import View
from django.db.models import F
from libs import JsonParser, Argument, json_response, auth
from apps.app.models import App, Deploy, DeployExtend1, DeployExtend2
from apps.config.models import Config, ConfigHistory, Service
from apps.app.utils import fetch_versions, remove_repo
from apps.setting.utils import AppSetting
import json
import re


class AppView(View):
    def get(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False)
        ).parse(request.GET)
        if error is None:
            if request.user.is_supper:
                apps = App.objects.all()
            else:
                ids = request.user.deploy_perms['apps']
                apps = App.objects.filter(id__in=ids)

            if form.id:
                app = apps.filter(pk=form.id).first()
                return json_response(app)
            return json_response(apps)
        return json_response(error=error)

    @auth('deploy.app.add|deploy.app.edit|config.app.add|config.app.edit')
    def post(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False),
            Argument('name', help='请输入服务名称'),
            Argument('key', help='请输入唯一标识符'),
            Argument('desc', required=False)
        ).parse(request.body)
        if error is None:
            if not re.fullmatch(r'\w+', form.key, re.ASCII):
                return json_response(error='标识符必须为字母、数字和下划线的组合')

            app = App.objects.filter(key=form.key).first()
            if app and app.id != form.id:
                return json_response(error='该识符已存在,请更改后重试')
            service = Service.objects.filter(key=form.key).first()
            if service:
                return json_response(error=f'该标识符已被服务 {service.name} 使用,请更改后重试')
            if form.id:
                App.objects.filter(pk=form.id).update(**form)
            else:
                app = App.objects.create(created_by=request.user, **form)
                app.sort_id = app.id
                app.save()
        return json_response(error=error)

    @auth('deploy.app.edit|config.app.edit_config')
    def patch(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='参数错误'),
            Argument('rel_apps', type=list, required=False),
            Argument('rel_services', type=list, required=False),
            Argument('sort', filter=lambda x: x in ('up', 'down'), required=False)
        ).parse(request.body)
        if error is None:
            app = App.objects.filter(pk=form.id).first()
            if not app:
                return json_response(error='未找到指定应用')
            if form.rel_apps is not None:
                app.rel_apps = json.dumps(form.rel_apps)
            if form.rel_services is not None:
                app.rel_services = json.dumps(form.rel_services)
            if form.sort:
                if form.sort == 'up':
                    tmp = App.objects.filter(sort_id__gt=app.sort_id).last()
                else:
                    tmp = App.objects.filter(sort_id__lt=app.sort_id).first()
                if tmp:
                    tmp.sort_id, app.sort_id = app.sort_id, tmp.sort_id
                    tmp.save()
            app.save()
        return json_response(error=error)

    @auth('deploy.app.del|config.app.del')
    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='请指定操作对象')
        ).parse(request.GET)
        if error is None:
            if Deploy.objects.filter(app_id=form.id).exists():
                return json_response(error='该应用在应用发布中已存在关联的发布配置,请删除相关发布配置后再尝试删除')
            # auto delete configs
            Config.objects.filter(type='app', o_id=form.id).delete()
            ConfigHistory.objects.filter(type='app', o_id=form.id).delete()
            for app in App.objects.filter(rel_apps__isnull=False):
                rel_apps = json.loads(app.rel_apps)
                if form.id in rel_apps:
                    rel_apps.remove(form.id)
                    app.rel_apps = json.dumps(rel_apps)
                    app.save()
            App.objects.filter(pk=form.id).delete()
        return json_response(error=error)


class DeployView(View):
    @auth('deploy.app.view|deploy.request.view')
    def get(self, request):
        form, error = JsonParser(
            Argument('app_id', type=int, required=False)
        ).parse(request.GET, True)
        if not request.user.is_supper:
            perms = request.user.deploy_perms
            form.app_id__in = perms['apps']
            form.env_id__in = perms['envs']
        deploys = Deploy.objects.filter(**form) \
            .annotate(app_name=F('app__name'), app_key=F('app__key')) \
            .order_by('-app__sort_id')
        return json_response(deploys)

    @auth('deploy.app.edit')
    def post(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False),
            Argument('app_id', type=int, help='请选择应用'),
            Argument('env_id', type=int, help='请选择环境'),
            Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'),
            Argument('rst_notify', type=dict, help='请选择发布结果通知方式'),
            Argument('extend', filter=lambda x: x in dict(Deploy.EXTENDS), help='请选择发布类型'),
            Argument('is_parallel', type=bool, default=True),
            Argument('is_audit', type=bool, default=False)
        ).parse(request.body)
        if error is None:
            deploy = Deploy.objects.filter(app_id=form.app_id, env_id=form.env_id).first()
            if deploy and deploy.id != form.id:
                return json_response(error='应用在该环境下已经存在发布配置')
            form.host_ids = json.dumps(form.host_ids)
            form.rst_notify = json.dumps(form.rst_notify)
            if form.extend == '1':
                extend_form, error = JsonParser(
                    Argument('git_repo', handler=str.strip, help='请输入git仓库地址'),
                    Argument('dst_dir', handler=str.strip, help='请输入发布部署路径'),
                    Argument('dst_repo', handler=str.strip, help='请输入发布存储路径'),
                    Argument('versions', type=int, filter=lambda x: x > 0, help='请输入发布保留版本数量'),
                    Argument('filter_rule', type=dict, help='参数错误'),
                    Argument('hook_pre_server', handler=str.strip, default=''),
                    Argument('hook_post_server', handler=str.strip, default=''),
                    Argument('hook_pre_host', handler=str.strip, default=''),
                    Argument('hook_post_host', handler=str.strip, default='')
                ).parse(request.body)
                if error:
                    return json_response(error=error)
                extend_form.dst_dir = extend_form.dst_dir.rstrip('/')
                extend_form.filter_rule = json.dumps(extend_form.filter_rule)
                if form.id:
                    extend = DeployExtend1.objects.filter(deploy_id=form.id).first()
                    if extend.git_repo != extend_form.git_repo:
                        remove_repo(form.id)
                    Deploy.objects.filter(pk=form.id).update(**form)
                    DeployExtend1.objects.filter(deploy_id=form.id).update(**extend_form)
                else:
                    deploy = Deploy.objects.create(created_by=request.user, **form)
                    DeployExtend1.objects.create(deploy=deploy, **extend_form)
            elif form.extend == '2':
                extend_form, error = JsonParser(
                    Argument('server_actions', type=list, help='请输入执行动作'),
                    Argument('host_actions', type=list, help='请输入执行动作')
                ).parse(request.body)
                if error:
                    return json_response(error=error)
                if len(extend_form.server_actions) + len(extend_form.host_actions) == 0:
                    return json_response(error='请至少设置一个执行的动作')
                extend_form.require_upload = any(x.get('src_mode') == '1' for x in extend_form.host_actions)
                extend_form.server_actions = json.dumps(extend_form.server_actions)
                extend_form.host_actions = json.dumps(extend_form.host_actions)
                if form.id:
                    Deploy.objects.filter(pk=form.id).update(**form)
                    DeployExtend2.objects.filter(deploy_id=form.id).update(**extend_form)
                else:
                    deploy = Deploy.objects.create(created_by=request.user, **form)
                    DeployExtend2.objects.create(deploy=deploy, **extend_form)
        return json_response(error=error)

    @auth('deploy.app.del')
    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='请指定操作对象')
        ).parse(request.GET)
        if error is None:
            deploy = Deploy.objects.get(pk=form.id)
            if deploy.deployrequest_set.exists():
                return json_response(error='已存在关联的发布记录,请删除关联的发布记录后再尝试删除发布配置')
            for item in deploy.repository_set.all():
                item.delete()
            deploy.delete()
        return json_response(error=error)


@auth('deploy.app.config|deploy.repository.add|deploy.request.add|deploy.request.edit')
def get_versions(request, d_id):
    deploy = Deploy.objects.filter(pk=d_id).first()
    if not deploy:
        return json_response(error='未找到指定应用')
    if deploy.extend == '2':
        return json_response(error='该应用不支持此操作')
    branches, tags = fetch_versions(deploy)
    return json_response({'branches': branches, 'tags': tags})


@auth('deploy.app.config|deploy.app.edit')
def kit_key(request):
    form, error = JsonParser(
        Argument('key', filter=lambda x: x in ('api_key', 'public_key'), help='参数错误')
    ).parse(request.body)
    if error is None:
        api_key = AppSetting.get_default(form.key)
        return json_response(api_key)
    return json_response(error=error)


================================================
FILE: spug_api/apps/config/__init__.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.


================================================
FILE: spug_api/apps/config/models.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.db import models
from libs import ModelMixin, human_datetime
from apps.account.models import User


class Environment(models.Model, ModelMixin):
    name = models.CharField(max_length=50)
    key = models.CharField(max_length=50)
    desc = models.CharField(max_length=255, null=True)
    sort_id = models.IntegerField(default=0, db_index=True)
    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey(User, on_delete=models.PROTECT)

    def __repr__(self):
        return f'<Environment {self.name!r}>'

    class Meta:
        db_table = 'environments'
        ordering = ('-sort_id',)


class Service(models.Model, ModelMixin):
    name = models.CharField(max_length=50)
    key = models.CharField(max_length=50, unique=True)
    desc = models.CharField(max_length=255, null=True)
    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey(User, on_delete=models.PROTECT)

    def __repr__(self):
        return f'<Service {self.name!r}>'

    class Meta:
        db_table = 'services'
        ordering = ('-id',)


class Config(models.Model, ModelMixin):
    TYPES = (
        ('app', 'App'),
        ('src', 'Service')
    )
    type = models.CharField(max_length=5, choices=TYPES)
    o_id = models.IntegerField()
    key = models.CharField(max_length=50)
    env = models.ForeignKey(Environment, on_delete=models.PROTECT)
    value = models.TextField(null=True)
    desc = models.CharField(max_length=255, null=True)
    is_public = models.BooleanField(default=False)
    updated_at = models.CharField(max_length=20)
    updated_by = models.ForeignKey(User, on_delete=models.PROTECT)

    def __repr__(self):
        return f'<Config {self.key!r}>'

    class Meta:
        db_table = 'configs'
        ordering = ('-key',)


class ConfigHistory(models.Model, ModelMixin):
    ACTIONS = (
        ('1', '新增'),
        ('2', '更新'),
        ('3', '删除')
    )
    type = models.CharField(max_length=5)
    o_id = models.IntegerField()
    key = models.CharField(max_length=50)
    env_id = models.IntegerField()
    value = models.TextField(null=True)
    desc = models.CharField(max_length=255, null=True)
    is_public = models.BooleanField()
    old_value = models.TextField(null=True)
    action = models.CharField(max_length=2, choices=ACTIONS)
    updated_at = models.CharField(max_length=20)
    updated_by = models.ForeignKey(User, on_delete=models.PROTECT)

    def __repr__(self):
        return f'<ConfigHistory {self.key!r}>'

    class Meta:
        db_table = 'config_histories'
        ordering = ('key',)


================================================
FILE: spug_api/apps/config/urls.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.urls import path

from .views import *

urlpatterns = [
    path('', ConfigView.as_view()),
    path('parse/json/', parse_json),
    path('parse/text/', parse_text),
    path('diff/', post_diff),
    path('environment/', EnvironmentView.as_view()),
    path('service/', ServiceView.as_view()),
    path('history/', HistoryView.as_view()),
]


================================================
FILE: spug_api/apps/config/utils.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from apps.config.models import Config, Service
from apps.app.models import App
import json


def compose_configs(app, env_id, no_prefix=False):
    configs = dict()
    # app own configs
    for item in Config.objects.filter(type='app', o_id=app.id, env_id=env_id).only('key', 'value'):
        key = item.key if no_prefix else f'{app.key}_{item.key}'
        configs[key] = item.value

    # relation app public configs
    if app.rel_apps:
        app_ids = json.loads(app.rel_apps)
        if app_ids:
            id_key_map = {x.id: x.key for x in App.objects.filter(id__in=app_ids)}
            for item in Config.objects.filter(type='app', o_id__in=app_ids, env_id=env_id, is_public=True) \
                    .only('key', 'value'):
                key = item.key if no_prefix else f'{id_key_map[item.o_id]}_{item.key}'
                configs[key] = item.value

    # relation service configs
    if app.rel_services:
        src_ids = json.loads(app.rel_services)
        if src_ids:
            id_key_map = {x.id: x.key for x in Service.objects.filter(id__in=src_ids)}
            for item in Config.objects.filter(type='src', o_id__in=src_ids, env_id=env_id).only('key', 'value'):
                key = item.key if no_prefix else f'{id_key_map[item.o_id]}_{item.key}'
                configs[key] = item.value
    return configs


================================================
FILE: spug_api/apps/config/views.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.views.generic import View
from django.db.models import F
from libs import json_response, JsonParser, Argument, auth
from apps.app.models import Deploy, App
from apps.repository.models import Repository
from apps.config.models import *
import json
import re


class EnvironmentView(View):
    def get(self, request):
        query = {}
        if not request.user.is_supper:
            query['id__in'] = request.user.deploy_perms['envs']
        envs = Environment.objects.filter(**query)
        return json_response(envs)

    @auth('config.env.add|config.env.edit')
    def post(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False),
            Argument('name', help='请输入环境名称'),
            Argument('key', help='请输入唯一标识符'),
            Argument('desc', required=False)
        ).parse(request.body)
        if error is None:
            if not re.fullmatch(r'\w+', form.key, re.ASCII):
                return json_response(error='标识符必须为字母、数字和下划线的组合')

            env = Environment.objects.filter(key=form.key).first()
            if env and env.id != form.id:
                return json_response(error=f'唯一标识符 {form.key} 已存在,请更改后重试')
            if form.id:
                Environment.objects.filter(pk=form.id).update(**form)
            else:
                env = Environment.objects.create(created_by=request.user, **form)
                env.sort_id = env.id
                env.save()
        return json_response(error=error)

    @auth('config.env.edit')
    def patch(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='参数错误'),
            Argument('sort', filter=lambda x: x in ('up', 'down'), required=False)
        ).parse(request.body)
        if error is None:
            env = Environment.objects.filter(pk=form.id).first()
            if not env:
                return json_response(error='未找到指定环境')
            if form.sort:
                if form.sort == 'up':
                    tmp = Environment.objects.filter(sort_id__gt=env.sort_id).last()
                else:
                    tmp = Environment.objects.filter(sort_id__lt=env.sort_id).first()
                if tmp:
                    tmp.sort_id, env.sort_id = env.sort_id, tmp.sort_id
                    tmp.save()
            env.save()
        return json_response(error=error)

    @auth('config.env.del')
    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='请指定操作对象')
        ).parse(request.GET)
        if error is None:
            if Deploy.objects.filter(env_id=form.id).exists():
                return json_response(error='该环境已关联了发布配置,请删除相关发布配置后再尝试删除')
            if Repository.objects.filter(env_id=form.id).exists():
                return json_response(error='该环境关联了构建记录,请在删除应用发布/构建仓库中相关记录后再尝试')
            # auto delete configs
            Config.objects.filter(env_id=form.id).delete()
            ConfigHistory.objects.filter(env_id=form.id).delete()
            Environment.objects.filter(pk=form.id).delete()
        return json_response(error=error)


class ServiceView(View):
    @auth('config.src.view')
    def get(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False)
        ).parse(request.GET)
        if error is None:
            if form.id:
                service = Service.objects.get(pk=form.id)
                return json_response(service)
            services = Service.objects.all()
            return json_response(services)
        return json_response(error=error)

    @auth('config.src.add|config.src.edit')
    def post(self, request):
        form, error = JsonParser(
            Argument('id', type=int, required=False),
            Argument('name', help='请输入服务名称'),
            Argument('key', help='请输入唯一标识符'),
            Argument('desc', required=False)
        ).parse(request.body)
        if error is None:
            if not re.fullmatch(r'\w+', form.key, re.ASCII):
                return json_response(error='标识符必须为字母、数字和下划线的组合')

            service = Service.objects.filter(key=form.key).first()
            if service and service.id != form.id:
                return json_response(error='该标识符已存在,请更改后重试')
            app = App.objects.filter(key=form.key).first()
            if app:
                return json_response(error=f'该标识符已被应用 {app.name} 使用,请更改后重试')
            if form.id:
                Service.objects.filter(pk=form.id).update(**form)
            else:
                Service.objects.create(created_by=request.user, **form)
        return json_response(error=error)

    @auth('config.src.del')
    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='请指定操作对象')
        ).parse(request.GET)
        if error is None:
            rel_apps = []
            for app in App.objects.filter(rel_services__isnull=False):
                rel_services = json.loads(app.rel_services)
                if form.id in rel_services:
                    rel_apps.append(app.name)
            if rel_apps:
                return json_response(
                    error=f'该服务在配置中心已被 "{", ".join(rel_apps)}" 依赖,请解除依赖关系后再尝试删除。')
            # auto delete configs
            Config.objects.filter(type='src', o_id=form.id).delete()
            ConfigHistory.objects.filter(type='src', o_id=form.id).delete()
            Service.objects.filter(pk=form.id).delete()
        return json_response(error=error)


class ConfigView(View):
    @auth('config.src.view_config|config.app.view_config')
    def get(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='未指定操作对象'),
            Argument('type', filter=lambda x: x in dict(Config.TYPES), help='缺少必要参数'),
            Argument('env_id', type=int, help='缺少必要参数'),
        ).parse(request.GET)
        if error is None:
            form.o_id, data = form.pop('id'), []
            for item in Config.objects.filter(**form).annotate(update_user=F('updated_by__nickname')):
                tmp = item.to_dict()
                tmp['update_user'] = item.update_user
                data.append(tmp)
            return json_response(data)
        return json_response(error=error)

    @auth('config.src.edit_config|config.app.edit_config')
    def post(self, request):
        form, error = JsonParser(
            Argument('o_id', type=int, help='缺少必要参数'),
            Argument('type', filter=lambda x: x in dict(Config.TYPES), help='缺少必要参数'),
            Argument('envs', type=list, filter=lambda x: len(x), help='请选择环境'),
            Argument('key', help='请输入Key'),
            Argument('is_public', type=bool, help='缺少必要参数'),
            Argument('value', type=str, default=''),
            Argument('desc', required=False)
        ).parse(request.body)
        if error is None:
            form.value = form.value.strip()
            form.updated_at = human_datetime()
            form.updated_by = request.user
            envs = form.pop('envs')
            for env_id in envs:
                cf = Config.objects.filter(o_id=form.o_id, type=form.type, env_id=env_id, key=form.key).first()
                if cf:
                    raise Exception(f'{cf.env.name} 中已存在该Key')
                Config.objects.create(env_id=env_id, **form)
                ConfigHistory.objects.create(action='1', env_id=env_id, **form)
        return json_response(error=error)

    @auth('config.src.edit_config|config.app.edit_config')
    def patch(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='缺少必要参数'),
            Argument('value', type=str, default=''),
            Argument('is_public', type=bool, help='缺少必要参数'),
            Argument('desc', required=False)
        ).parse(request.body)
        if error is None:
            form.value = form.value.strip()
            config = Config.objects.filter(pk=form.id).first()
            if not config:
                return json_response(error='未找到指定对象')
            config.desc = form.desc
            config.is_public = form.is_public
            if config.value != form.value:
                old_value = config.value
                config.value = form.value
                config.updated_at = human_datetime()
                config.updated_by = request.user
                ConfigHistory.objects.create(
                    action='2',
                    old_value=old_value,
                    **config.to_dict(excludes=('id',)))
            config.save()
        return json_response(error=error)

    @auth('config.src.edit_config|config.app.edit_config')
    def delete(self, request):
        form, error = JsonParser(
            Argument('id', type=int, help='未指定操作对象')
        ).parse(request.GET)
        if error is None:
            config = Config.objects.filter(pk=form.id).first()
            if config:
                ConfigHistory.objects.create(
                    action='3',
                    old_value=config.value,
                    value='',
                    updated_at=human_datetime(),
                    updated_by=request.user,
                    **config.to_dict(excludes=('id', 'value', 'updated_at', 'updated_by_id'))
                )
                config.delete()
        return json_response(error=error)


class HistoryView(View):
    @auth('config.src.view_config|config.app.view_config')
    def post(self, request):
        form, error = JsonParser(
            Argument('o_id', type=int, help='缺少必要参数'),
            Argument('env_id', type=int, help='缺少必要参数'),
            Argument('type', filter=lambda x: x in dict(Config.TYPES), help='缺少必要参数')
        ).parse(request.body)
        if error is None:
            data = []
            for item in ConfigHistory.objects.filter(**form).annotate(update_user=F('updated_by__nickname')):
                tmp = item.to_dict()
                tmp['action_alias'] = item.get_action_display()
                tmp['update_user'] = item.update_user
                data.append(tmp)
            return json_response(data)
        return json_response(error=error)


@auth('config.src.view_config|config.app.view_config')
def post_diff(request):
    form, error = JsonParser(
        Argument('o_id', type=int, help='缺少必要参数'),
        Argument('type', filter=lambda x: x in dict(Config.TYPES), help='缺少必要参数'),
        Argument('envs', type=list, filter=lambda x: len(x), help='缺少必要参数'),
    ).parse(request.body)
    if error is None:
        data, form.env_id__in = {}, form.pop('envs')
        for item in Config.objects.filter(**form).order_by('key'):
            if item.key in data:
                data[item.key][item.env_id] = item.value
            else:
                data[item.key] = {'key': item.key, item.env_id: item.value}
        return json_response(list(data.values()))
    return json_response(error=error)


@auth('config.src.edit_config|config.app.edit_config')
def parse_json(request):
    form, error = JsonParser(
        Argument('o_id', type=int, help='缺少必要参数'),
        Argument('type', filter=lambda x: x in dict(Config.TYPES), help='缺少必要参数'),
        Argument('env_id', type=int, help='缺少必要参数'),
        Argument('data', type=dict, help='缺少必要参数')
    ).parse(request.body)
    if error is None:
        data = form.pop('data')
        _parse(request, form, data)
    return json_response(error=error)


@auth('config.src.edit_config|config.app.edit_config')
def parse_text(request):
    form, error = JsonParser(
        Argument('o_id', type=int, help='缺少必要参数'),
        Argument('type', filter=lambda x: x in dict(Config.TYPES), help='缺少必要参数'),
        Argument('env_id', type=int, help='缺少必要参数'),
        Argument('data', handler=str.strip, help='缺少必要参数')
    ).parse(request.body)
    if error is None:
        data = {}
        for line in form.pop('data').split('\n'):
            line = line.strip()
            if not line or line[0] in ('#', ';'):
                continue
            fields = line.split('=', 1)
            if len(fields) != 2 or fields[0].strip() == '':
                return json_response(error=f'解析配置{line!r}失败,确认其遵循 key = value 格式')
            data[fields[0].strip()] = fields[1].strip()
        _parse(request, form, data)
    return json_response(error=error)


def _parse(request, query, data):
    for item in Config.objects.filter(**query):
        if item.key in data:
            value = _filter_value(data.pop(item.key))
            if item.value != value:
                old_value = item.value
                item.value = value
                item.updated_at = human_datetime()
                item.updated_by = request.user
                item.save()
                ConfigHistory.objects.create(
                    action='2',
                    old_value=old_value,
                    **item.to_dict(excludes=('id',)))
        else:
            ConfigHistory.objects.create(
                action='3',
                old_value=item.value,
                value='',
                updated_at=human_datetime(),
                updated_by=request.user,
                **item.to_dict(excludes=('id', 'value', 'updated_at', 'updated_by_id'))
            )
            item.delete()
    for key, value in data.items():
        query.key = key
        query.is_public = False
        query.value = _filter_value(value)
        query.updated_at = human_datetime()
        query.updated_by = request.user
        Config.objects.create(**query)
        ConfigHistory.objects.create(action='1', **query)


def _filter_value(value):
    if isinstance(value, (str, int)):
        value = str(value).strip()
    else:
        value = json.dumps(value)
    return value


================================================
FILE: spug_api/apps/deploy/__init__.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.


================================================
FILE: spug_api/apps/deploy/helper.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.template.defaultfilters import filesizeformat
from libs.utils import human_datetime, render_str, str_decode
from libs.spug import Notification
from apps.host.models import Host
from functools import partial
import subprocess
import json
import os


class SpugError(Exception):
    pass


class Helper:
    def __init__(self, rds, key):
        self.rds = rds
        self.key = key
        self.callback = []

    @classmethod
    def make(cls, rds, key, host_ids=None):
        if host_ids:
            counter, tmp_key = 0, f'{key}_tmp'
            data = rds.lrange(key, counter, counter + 9)
            while data:
                for item in data:
                    counter += 1
                    print(item)
                    tmp = json.loads(item.decode())
                    if tmp['key'] not in host_ids:
                        rds.rpush(tmp_key, item)
                data = rds.lrange(key, counter, counter + 9)
            rds.delete(key)
            if rds.exists(tmp_key):
                rds.rename(tmp_key, key)
        else:
            rds.delete(key)
        return cls(rds, key)

    @classmethod
    def _make_dd_notify(cls, url, action, req, version, host_str):
        texts = [
            f'**申请标题:** {req.name}',
            f'**应用名称:** {req.deploy.app.name}',
            f'**应用版本:** {version}',
            f'**发布环境:** {req.deploy.env.name}',
            f'**发布主机:** {host_str}',
        ]
        if action == 'approve_req':
            texts.insert(0, '## %s ## ' % '发布审核申请')
            texts.extend([
                f'**申请人员:** {req.created_by.nickname}',
                f'**申请时间:** {human_datetime()}',
                '> 来自 Spug运维平台'
            ])
        elif action == 'approve_rst':
            color, text = ('#008000', '通过') if req.status == '1' else ('#f90202', '驳回')
            texts.insert(0, '## %s ## ' % '发布审核结果')
            texts.extend([
                f'**审核人员:** {req.approve_by.nickname}',
                f'**审核结果:** <font color="{color}">{text}</font>',
                f'**审核意见:** {req.reason or ""}',
                f'**审核时间:** {human_datetime()}',
                '> 来自 Spug运维平台'
            ])
        else:
            color, text = ('#008000', '成功') if req.status == '3' else ('#f90202', '失败')
            texts.insert(0, '## %s ## ' % '发布结果通知')
            if req.approve_at:
                texts.append(f'**审核人员:** {req.approve_by.nickname}')
            do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
            texts.extend([
                f'**执行人员:** {do_user}',
                f'**发布结果:** <font color="{color}">{text}</font>',
                f'**发布时间:** {human_datetime()}',
                '> 来自 Spug运维平台'
            ])
        data = {
            'msgtype': 'markdown',
            'markdown': {
                'title': 'Spug 发布消息通知',
                'text': '\n\n'.join(texts)
            },
            'at': {
                'isAtAll': True
            }
        }
        Notification.handle_request(url, data, 'dd')

    @classmethod
    def _make_wx_notify(cls, url, action, req, version, host_str):
        texts = [
            f'申请标题: {req.name}',
            f'应用名称: {req.deploy.app.name}',
            f'应用版本: {version}',
            f'发布环境: {req.deploy.env.name}',
            f'发布主机: {host_str}',
        ]

        if action == 'approve_req':
            texts.insert(0, '## %s' % '发布审核申请')
            texts.extend([
                f'申请人员: {req.created_by.nickname}',
                f'申请时间: {human_datetime()}',
                '> 来自 Spug运维平台'
            ])
        elif action == 'approve_rst':
            color, text = ('info', '通过') if req.status == '1' else ('warning', '驳回')
            texts.insert(0, '## %s' % '发布审核结果')
            texts.extend([
                f'审核人员: {req.approve_by.nickname}',
                f'审核结果: <font color="{color}">{text}</font>',
                f'审核意见: {req.reason or ""}',
                f'审核时间: {human_datetime()}',
                '> 来自 Spug运维平台'
            ])
        else:
            color, text = ('info', '成功') if req.status == '3' else ('warning', '失败')
            texts.insert(0, '## %s' % '发布结果通知')
            if req.approve_at:
                texts.append(f'审核人员: {req.approve_by.nickname}')
            do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
            texts.extend([
                f'执行人员: {do_user}',
                f'发布结果: <font color="{color}">{text}</font>',
                f'发布时间: {human_datetime()}',
                '> 来自 Spug运维平台'
            ])
        data = {
            'msgtype': 'markdown',
            'markdown': {
                'content': '\n'.join(texts)
            }
        }
        Notification.handle_request(url, data, 'wx')

    @classmethod
    def _make_fs_notify(cls, url, action, req, version, host_str):
        texts = [
            f'申请标题: {req.name}',
            f'应用名称: {req.deploy.app.name}',
            f'应用版本: {version}',
            f'发布环境: {req.deploy.env.name}',
            f'发布主机: {host_str}',
        ]

        if action == 'approve_req':
            title = '发布审核申请'
            texts.extend([
                f'申请人员: {req.created_by.nickname}',
                f'申请时间: {human_datetime()}',
            ])
        elif action == 'approve_rst':
            title = '发布审核结果'
            text = '通过' if req.status == '1' else '驳回'
            texts.extend([
                f'审核人员: {req.approve_by.nickname}',
                f'审核结果: {text}',
                f'审核意见: {req.reason or ""}',
                f'审核时间: {human_datetime()}',
            ])
        else:
            title = '发布结果通知'
            text = '成功 ✅' if req.status == '3' else '失败 ❗'
            if req.approve_at:
                texts.append(f'审核人员: {req.approve_by.nickname}')
            do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
            texts.extend([
                f'执行人员: {do_user}',
                f'发布结果: {text}',
                f'发布时间: {human_datetime()}',
            ])
        data = {
            'msg_type': 'post',
            'content': {
                'post': {
                    'zh_cn': {
                        'title': title,
                        'content': [[{'tag': 'text', 'text': x}] for x in texts] + [[{'tag': 'at', 'user_id': 'all'}]]
                    }
                }
            }
        }
        Notification.handle_request(url, data, 'fs')

    @classmethod
    def send_deploy_notify(cls, req, action=None):
        rst_notify = json.loads(req.deploy.rst_notify)
        host_ids = json.loads(req.host_ids) if isinstance(req.host_ids, str) else req.host_ids
        if rst_notify['mode'] != '0' and rst_notify.get('value'):
            url = rst_notify['value']
            version = req.version
            hosts = [{'id': x.id, 'name': x.name} for x in Host.objects.filter(id__in=host_ids)]
            host_str = ', '.join(x['name'] for x in hosts[:2])
            if len(hosts) > 2:
                host_str += f'等{len(hosts)}台主机'
            if rst_notify['mode'] == '1':
                cls._make_dd_notify(url, action, req, version, host_str)
            elif rst_notify['mode'] == '2':
                data = {
                    'action': action,
                    'req_id': req.id,
                    'req_name': req.name,
                    'app_id': req.deploy.app_id,
                    'app_name': req.deploy.app.name,
                    'env_id': req.deploy.env_id,
                    'env_name': req.deploy.env.name,
                    'status': req.status,
                    'reason': req.reason,
                    'version': version,
                    'targets': hosts,
                    'is_success': req.status == '3',
                    'created_at': human_datetime()
                }
                Notification.handle_request(url, data)
            elif rst_notify['mode'] == '3':
                cls._make_wx_notify(url, action, req, version, host_str)
            elif rst_notify['mode'] == '4':
                cls._make_fs_notify(url, action, req, version, host_str)
            else:
                raise NotImplementedError

    def add_callback(self, func):
        self.callback.append(func)

    def parse_filter_rule(self, data: str, sep='\n', env=None):
        data, files = data.strip(), []
        if data:
            for line in data.split(sep):
                line = line.strip()
                if line and not line.startswith('#'):
                    files.append(render_str(line, env))
        return files

    def _send(self, message):
        self.rds.rpush(self.key, json.dumps(message))

    def send_info(self, key, message):
        if message:
            self._send({'key': key, 'data': message})

    def send_error(self, key, message, with_break=True):
        message = f'\r\n\033[31m{message}\033[0m'
        self._send({'key': key, 'status': 'error', 'data': message})
        if with_break:
            raise SpugError

    def send_step(self, key, step, data):
        self._send({'key': key, 'step': step, 'data': data})

    def clear(self):
        self.rds.delete(f'{self.key}_tmp')
        # save logs for two weeks
        self.rds.expire(self.key, 14 * 24 * 60 * 60)
        self.rds.close()
        # callback
        for func in self.callback:
            func()

    def progress_callback(self, key):
        def func(k, n, t):
            message = f'\r         {filesizeformat(n):<8}/{filesizeformat(t):>8}  '
            self.send_info(k, message)

        self.send_info(key, '\r\n')
        return partial(func, key)

    def local(self, command, env=None):
        if env:
            env = dict(env.items())
            env.update(os.environ)
        task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        message = b''
        while True:
            output = task.stdout.read(1)
            if not output:
                break
            if output in (b'\r', b'\n'):
                message += b'\r\n' if output == b'\n' else b'\r'
                message = str_decode(message)
                self.send_info('local', message)
                message = b''
            else:
                message += output
        if task.wait() != 0:
            self.send_error('local', f'exit code: {task.returncode}')

    def remote(self, key, ssh, command, env=None):
        code = -1
        for code, out in ssh.exec_command_with_stream(command, environment=env):
            self.send_info(key, out)
        if code != 0:
            self.send_error(key, f'exit code: {code}')

    def remote_raw(self, key, ssh, command):
        code, out = ssh.exec_command_raw(command)
        if code != 0:
            self.send_error(key, f'exit code: {code}, {out}')


================================================
FILE: spug_api/apps/deploy/models.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.db import models
from django.conf import settings
from libs import ModelMixin, human_datetime
from apps.account.models import User
from apps.app.models import Deploy
from apps.repository.models import Repository
import json
import os


class DeployRequest(models.Model, ModelMixin):
    STATUS = (
        ('-3', '发布异常'),
        ('-1', '已驳回'),
        ('0', '待审核'),
        ('1', '待发布'),
        ('2', '发布中'),
        ('3', '发布成功'),
    )
    TYPES = (
        ('1', '正常发布'),
        ('2', '回滚'),
        ('3', '自动发布'),
    )
    deploy = models.ForeignKey(Deploy, on_delete=models.CASCADE)
    repository = models.ForeignKey(Repository, null=True, on_delete=models.SET_NULL)
    name = models.CharField(max_length=100)
    type = models.CharField(max_length=2, choices=TYPES, default='1')
    extra = models.TextField()
    host_ids = models.TextField()
    desc = models.CharField(max_length=255, null=True)
    status = models.CharField(max_length=2, choices=STATUS)
    reason = models.CharField(max_length=255, null=True)
    version = models.CharField(max_length=100, null=True)
    spug_version = models.CharField(max_length=50, null=True)
    plan = models.DateTimeField(null=True)
    fail_host_ids = models.TextField(default='[]')

    created_at = models.CharField(max_length=20, default=human_datetime)
    created_by = models.ForeignKey(User, models.PROTECT, related_name='+')
    approve_at = models.CharField(max_length=20, null=True)
    approve_by = models.ForeignKey(User, models.PROTECT, related_name='+', null=True)
    do_at = models.CharField(max_length=20, null=True)
    do_by = models.ForeignKey(User, models.PROTECT, related_name='+', null=True)

    @property
    def is_quick_deploy(self):
        if self.type in ('1', '3') and self.deploy.extend == '1' and self.extra:
            extra = json.loads(self.extra)
            return extra[0] in ('branch', 'tag')
        return False

    def delete(self, using=None, keep_parents=False):
        super().delete(using, keep_parents)
        if self.repository_id:
            if not DeployRequest.objects.filter(repository=self.repository).exists():
                self.repository.delete()
        if self.deploy.extend == '2':
            try:
                os.remove(os.path.join(settings.REPOS_DIR, str(self.deploy_id), self.spug_version))
            except FileNotFoundError:
                pass

    def __repr__(self):
        return f'<DeployRequest name={self.name}>'

    class Meta:
        db_table = 'deploy_requests'
        ordering = ('-id',)


================================================
FILE: spug_api/apps/deploy/urls.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.urls import path

from .views import *

urlpatterns = [
    path('request/', RequestView.as_view()),
    path('request/info/', get_request_info),
    path('request/ext1/', post_request_ext1),
    path('request/ext1/rollback/', post_request_ext1_rollback),
    path('request/ext2/', post_request_ext2),
    path('request/upload/', do_upload),
    path('request/<int:r_id>/', RequestDetailView.as_view()),
]


================================================
FILE: spug_api/apps/deploy/utils.py
================================================
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django_redis import get_redis_connection
from django.conf import settings
from django.db import close_old_connections
from libs.utils import AttrDict, human_time, render_str
from apps.host.models import Host
from apps.config.utils import compose_configs
from apps.repository.models import Repository
from apps.repository.utils import dispatch as build_repository
from apps.deploy.models import DeployRequest
from apps.deploy.helper import Helper, SpugError
from concurrent import futures
from functools import partial
import json
import uuid
import os

REPOS_DIR = settings.REPOS_DIR
BUILD_DIR = settings.BUILD_DIR


def dispatch(req, fail_mode=False):
    rds = get_redis_connection()
    rds_key = f'{settings.REQUEST_KEY}:{req.id}'
    if fail_mode:
        req.host_ids = req.fail_host_ids
    req.fail_mode = fail_mode
    req.host_ids = json.loads(req.host_ids)
    req.fail_host_ids = req.host_ids[:]
    helper = Helper.make(rds, rds_key, req.host_ids if fail_mode else None)

    try:
        api_token = uuid.uuid4().hex
        rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
        env = AttrDict(
            SPUG_APP_NAME=req.deploy.app.name,
            SPUG_APP_KEY=req.deploy.app.key,
            SPUG_APP_ID=str(req.deploy.app_id),
            SPUG_REQUEST_ID=str(req.id),
            SPUG_REQUEST_NAME=req.name,
            SPUG_DEPLOY_ID=str(req.deploy.id),
            SPUG_ENV_ID=str(req.deploy.env_id),
            SPUG_ENV_KEY=req.deploy.env.key,
            SPUG_VERSION=req.version,
            SPUG_BUILD_VERSION=req.spug_version,
            SPUG_DEPLOY_TYPE=req.type,
            SPUG_API_TOKEN=api_token,
            SPUG_REPOS_DIR=REPOS_DIR,
        )
        # append configs
        configs = compose_configs(req.deploy.app, req.deploy.env_id)
        configs_env = {f'_SPUG_{k.upper()}': v for k, v in configs.items()}
        env.update(configs_env)

        if req.deploy.extend == '1':
            _ext1_deploy(req, helper, env)
        else:
            _ext2_deploy(req, helper, env)
        req.status = '3'
    except Exception as e:
        req.status = '-3'
        raise e
    finally:
        close_old_connections()
        DeployRequest.objects.filter(pk=req.id).update(
            status=req.status,
            repository=req.repository,
            fail_host_ids=json.dumps(req.fail_host_ids),
        )
        helper.clear()
        Helper.send_deploy_notify(req)


def _ext1_deploy(req, helper, env):
    if not req.repository_id:
        rep = Repository(
            app_id=req.deploy.app_id,
            env_id=req.deploy.env_id,
            deploy_id=req.deploy_id,
            version=req.version,
            spug_version=req.spug_version,
            extra=req.extra,
            remarks='SPUG AUTO MAKE',
            created_by_id=req.created_by_id
        )
        build_repository(rep, helper)
        req.repository = rep
    extras = json.loads(req.extra)
    if extras[0] == 'repository':
        extras = extras[1:]
    if extras[0] == 'branch':
        env.update(SPUG_GIT_BRANCH=extras[1], SPUG_GIT_COMMIT_ID=extras[2])
    else:
        env.update(SPUG_GIT_TAG=extras[1])
    if req.deploy.is_parallel:
        threads, latest_exception = [], None
        max_workers = max(10, os.cpu_count() * 5)
        with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
            for h_id in req.host_ids:
                new_env = AttrDict(env.items())
                t = executor.submit(_deploy_ext1_host, req, helper, h_id, new_env)
                t.h_id = h_id
                threads.append(t)
            for t in futures.as_completed(threads):
                exception = t.exception()
                if exception:
                    latest_exception = exception
                    if not isinstance(exception, SpugError):
                        helper.send_error(t.h_id, f'Exception: {exception}', False)
                else:
           
Download .txt
gitextract_da93zxoy/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   └── bug-report.md
│   └── workflows/
│       └── github_to_gitee.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs/
│   ├── FQA.md
│   ├── docker/
│   │   ├── Dockerfile
│   │   ├── docker-compose.yml
│   │   ├── entrypoint.sh
│   │   ├── init_spug
│   │   ├── nginx.conf
│   │   ├── redis.conf
│   │   ├── spug.ini
│   │   └── ssh_config
│   └── install.sh
├── spug_api/
│   ├── .gitignore
│   ├── apps/
│   │   ├── account/
│   │   │   ├── __init__.py
│   │   │   ├── history.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       ├── set.py
│   │   │   │       ├── update.py
│   │   │   │       ├── updatedb.py
│   │   │   │       └── user.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── alarm/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   ├── apis/
│   │   │   ├── __init__.py
│   │   │   ├── config.py
│   │   │   ├── deploy.py
│   │   │   └── urls.py
│   │   ├── app/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── config/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── deploy/
│   │   │   ├── __init__.py
│   │   │   ├── helper.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── exec/
│   │   │   ├── __init__.py
│   │   │   ├── executors.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       └── runworker.py
│   │   │   ├── models.py
│   │   │   ├── transfer.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   ├── file/
│   │   │   ├── __init__.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── home/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── navigation.py
│   │   │   ├── notice.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   ├── host/
│   │   │   ├── __init__.py
│   │   │   ├── add.py
│   │   │   ├── extend.py
│   │   │   ├── group.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── monitor/
│   │   │   ├── __init__.py
│   │   │   ├── executors.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       └── runmonitor.py
│   │   │   ├── models.py
│   │   │   ├── scheduler.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── notify/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   └── views.py
│   │   ├── repository/
│   │   │   ├── __init__.py
│   │   │   ├── models.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   ├── schedule/
│   │   │   ├── __init__.py
│   │   │   ├── builtin.py
│   │   │   ├── executors.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       └── runscheduler.py
│   │   │   ├── models.py
│   │   │   ├── scheduler.py
│   │   │   ├── urls.py
│   │   │   ├── utils.py
│   │   │   └── views.py
│   │   └── setting/
│   │       ├── __init__.py
│   │       ├── models.py
│   │       ├── urls.py
│   │       ├── user.py
│   │       ├── utils.py
│   │       └── views.py
│   ├── consumer/
│   │   ├── __init__.py
│   │   ├── consumers.py
│   │   ├── routing.py
│   │   └── utils.py
│   ├── libs/
│   │   ├── __init__.py
│   │   ├── channel.py
│   │   ├── decorators.py
│   │   ├── gitlib.py
│   │   ├── helper.py
│   │   ├── ldap.py
│   │   ├── mail.py
│   │   ├── middleware.py
│   │   ├── mixins.py
│   │   ├── parser.py
│   │   ├── push.py
│   │   ├── spug.py
│   │   ├── ssh.py
│   │   ├── utils.py
│   │   └── validators.py
│   ├── logs/
│   │   └── .gitkeep
│   ├── manage.py
│   ├── repos/
│   │   ├── .gitkeep
│   │   └── build/
│   │       └── .gitkeep
│   ├── requirements.txt
│   ├── spug/
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── routing.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── storage/
│   │   └── transfer/
│   │       └── .gitkeep
│   └── tools/
│       ├── migrate.py
│       ├── start-api.sh
│       ├── start-monitor.sh
│       ├── start-scheduler.sh
│       ├── start-worker.sh
│       ├── start-ws.sh
│       └── supervisor-spug.ini
└── spug_web/
    ├── .gitignore
    ├── README.md
    ├── config-overrides.js
    ├── jsconfig.json
    ├── package.json
    ├── public/
    │   ├── index.html
    │   ├── manifest.json
    │   ├── resource/
    │   │   └── 主机导入模板.xlsx
    │   └── robots.txt
    └── src/
        ├── App.js
        ├── components/
        │   ├── ACEditor.js
        │   ├── Action.js
        │   ├── AppSelector.js
        │   ├── AuthButton.js
        │   ├── AuthCard.js
        │   ├── AuthDiv.js
        │   ├── AuthFragment.js
        │   ├── Breadcrumb.js
        │   ├── Link.js
        │   ├── LinkButton.js
        │   ├── NotFound.js
        │   ├── SearchForm.js
        │   ├── StatisticsCard.js
        │   ├── TableCard.js
        │   ├── index.js
        │   └── index.module.less
        ├── gStore.js
        ├── index.js
        ├── index.less
        ├── layout/
        │   ├── Footer.js
        │   ├── Header.js
        │   ├── Notification.js
        │   ├── Sider.js
        │   ├── index.js
        │   └── layout.module.less
        ├── libs/
        │   ├── functools.js
        │   ├── history.js
        │   ├── http.js
        │   ├── index.js
        │   ├── libs.module.css
        │   └── router.js
        ├── pages/
        │   ├── alarm/
        │   │   ├── alarm/
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── contact/
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   └── group/
        │   │       ├── Form.js
        │   │       ├── Table.js
        │   │       ├── index.js
        │   │       └── store.js
        │   ├── config/
        │   │   ├── app/
        │   │   │   ├── Form.js
        │   │   │   ├── Rel.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── environment/
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── service/
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   └── setting/
        │   │       ├── DiffConfig.js
        │   │       ├── Form.js
        │   │       ├── JSONView.js
        │   │       ├── Record.js
        │   │       ├── TableView.js
        │   │       ├── TextView.js
        │   │       ├── index.js
        │   │       ├── index.module.css
        │   │       └── store.js
        │   ├── dashboard/
        │   │   ├── AlarmTrend.js
        │   │   ├── RequestTop.js
        │   │   ├── StatisticCard.js
        │   │   ├── index.js
        │   │   └── index.module.css
        │   ├── deploy/
        │   │   ├── app/
        │   │   │   ├── AddSelect.js
        │   │   │   ├── AutoDeploy.js
        │   │   │   ├── CloneConfirm.js
        │   │   │   ├── Ext1Form.js
        │   │   │   ├── Ext1Setup1.js
        │   │   │   ├── Ext1Setup2.js
        │   │   │   ├── Ext1Setup3.js
        │   │   │   ├── Ext2Form.js
        │   │   │   ├── Ext2Setup1.js
        │   │   │   ├── Ext2Setup2.js
        │   │   │   ├── Form.js
        │   │   │   ├── Repo.js
        │   │   │   ├── Table.js
        │   │   │   ├── Tips.js
        │   │   │   ├── index.js
        │   │   │   ├── index.module.css
        │   │   │   └── store.js
        │   │   ├── repository/
        │   │   │   ├── Console.js
        │   │   │   ├── Detail.js
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   ├── index.module.less
        │   │   │   └── store.js
        │   │   └── request/
        │   │       ├── Approve.js
        │   │       ├── BatchDelete.js
        │   │       ├── Ext1Console.js
        │   │       ├── Ext1Form.js
        │   │       ├── Ext2Console.js
        │   │       ├── Ext2Form.js
        │   │       ├── HostSelector.js
        │   │       ├── OutView.js
        │   │       ├── Rollback.js
        │   │       ├── Table.js
        │   │       ├── index.js
        │   │       ├── index.module.less
        │   │       └── store.js
        │   ├── exec/
        │   │   ├── task/
        │   │   │   ├── Output.js
        │   │   │   ├── Parameter.js
        │   │   │   ├── TemplateSelector.js
        │   │   │   ├── index.js
        │   │   │   ├── index.module.less
        │   │   │   └── store.js
        │   │   ├── template/
        │   │   │   ├── Form.js
        │   │   │   ├── Parameter.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   └── transfer/
        │   │       ├── Output.js
        │   │       ├── index.js
        │   │       ├── index.module.less
        │   │       └── store.js
        │   ├── home/
        │   │   ├── Nav.js
        │   │   ├── NavForm.js
        │   │   ├── Notice.js
        │   │   ├── Todo.js
        │   │   ├── index.js
        │   │   └── index.module.less
        │   ├── host/
        │   │   ├── BatchSync.js
        │   │   ├── CloudImport.js
        │   │   ├── Detail.js
        │   │   ├── Form.js
        │   │   ├── Group.js
        │   │   ├── IPAddress.js
        │   │   ├── Import.js
        │   │   ├── Selector.js
        │   │   ├── Sync.js
        │   │   ├── Table.js
        │   │   ├── icons/
        │   │   │   └── index.js
        │   │   ├── index.js
        │   │   ├── index.module.less
        │   │   ├── selector.module.less
        │   │   ├── store.js
        │   │   └── store2.js
        │   ├── login/
        │   │   ├── index.js
        │   │   └── login.module.css
        │   ├── monitor/
        │   │   ├── Form.js
        │   │   ├── MonitorCard.js
        │   │   ├── Step1.js
        │   │   ├── Step2.js
        │   │   ├── Table.js
        │   │   ├── index.js
        │   │   ├── index.module.less
        │   │   └── store.js
        │   ├── schedule/
        │   │   ├── Form.js
        │   │   ├── Info.js
        │   │   ├── Record.js
        │   │   ├── Step1.js
        │   │   ├── Step2.js
        │   │   ├── Step3.js
        │   │   ├── Table.js
        │   │   ├── index.js
        │   │   ├── index.module.css
        │   │   └── store.js
        │   ├── ssh/
        │   │   ├── FileManager.js
        │   │   ├── Setting.js
        │   │   ├── Terminal.js
        │   │   ├── index.js
        │   │   ├── index.module.less
        │   │   ├── setting.module.less
        │   │   └── themes.js
        │   ├── system/
        │   │   ├── account/
        │   │   │   ├── Form.js
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── login/
        │   │   │   ├── Table.js
        │   │   │   ├── index.js
        │   │   │   └── store.js
        │   │   ├── role/
        │   │   │   ├── DeployPerm.js
        │   │   │   ├── Form.js
        │   │   │   ├── HostPerm.js
        │   │   │   ├── PagePerm.js
        │   │   │   ├── RoleUsers.js
        │   │   │   ├── Table.js
        │   │   │   ├── codes.js
        │   │   │   ├── index.js
        │   │   │   ├── index.module.css
        │   │   │   └── store.js
        │   │   └── setting/
        │   │       ├── About.js
        │   │       ├── AlarmSetting.js
        │   │       ├── KeySetting.js
        │   │       ├── LDAPSetting.js
        │   │       ├── OpenService.js
        │   │       ├── PushSetting.js
        │   │       ├── SecuritySetting.js
        │   │       ├── index.js
        │   │       ├── index.module.css
        │   │       └── store.js
        │   └── welcome/
        │       ├── index/
        │       │   └── index.js
        │       └── info/
        │           ├── Basic.js
        │           ├── Reset.js
        │           ├── index.js
        │           ├── index.module.css
        │           └── store.js
        ├── routes.js
        ├── serviceWorker.js
        └── setupProxy.js
Download .txt
SYMBOL INDEX (936 symbols across 232 files)

FILE: spug_api/apps/account/history.py
  class HistoryView (line 9) | class HistoryView(AdminView):
    method get (line 10) | def get(self, request):

FILE: spug_api/apps/account/management/commands/set.py
  class Command (line 8) | class Command(BaseCommand):
    method add_arguments (line 11) | def add_arguments(self, parser):
    method echo_success (line 15) | def echo_success(self, msg):
    method echo_error (line 18) | def echo_error(self, msg):
    method print_help (line 21) | def print_help(self, *args):
    method handle (line 28) | def handle(self, *args, **options):

FILE: spug_api/apps/account/management/commands/update.py
  class Command (line 11) | class Command(BaseCommand):
    method handle (line 14) | def handle(self, *args, **options):

FILE: spug_api/apps/account/management/commands/updatedb.py
  class Command (line 9) | class Command(BaseCommand):
    method handle (line 12) | def handle(self, *args, **options):

FILE: spug_api/apps/account/management/commands/user.py
  class Command (line 9) | class Command(BaseCommand):
    method add_arguments (line 12) | def add_arguments(self, parser):
    method echo_success (line 19) | def echo_success(self, msg):
    method echo_error (line 22) | def echo_error(self, msg):
    method print_help (line 25) | def print_help(self, *args):
    method handle (line 34) | def handle(self, *args, **options):

FILE: spug_api/apps/account/models.py
  class User (line 11) | class User(models.Model, ModelMixin):
    method make_password (line 31) | def make_password(plain_password: str) -> str:
    method verify_password (line 34) | def verify_password(self, plain_password: str) -> bool:
    method get_perms_cache (line 37) | def get_perms_cache(self):
    method set_perms_cache (line 40) | def set_perms_cache(self, value=None):
    method page_perms (line 44) | def page_perms(self):
    method deploy_perms (line 58) | def deploy_perms(self):
    method group_perms (line 69) | def group_perms(self):
    method has_perms (line 76) | def has_perms(self, codes):
    method __repr__ (line 81) | def __repr__(self):
    class Meta (line 84) | class Meta:
  class Role (line 89) | class Role(models.Model, ModelMixin):
    method to_dict (line 98) | def to_dict(self, *args, **kwargs):
    method add_deploy_perm (line 106) | def add_deploy_perm(self, target, value):
    method clear_perms_cache (line 114) | def clear_perms_cache(self):
    method __repr__ (line 118) | def __repr__(self):
    class Meta (line 121) | class Meta:
  class History (line 126) | class History(models.Model, ModelMixin):
    class Meta (line 135) | class Meta:

FILE: spug_api/apps/account/utils.py
  function get_host_perms (line 8) | def get_host_perms(user):
  function has_host_perm (line 16) | def has_host_perm(user, target):
  function verify_password (line 25) | def verify_password(password):

FILE: spug_api/apps/account/views.py
  class UserView (line 22) | class UserView(AdminView):
    method get (line 23) | def get(self, request):
    method post (line 32) | def post(self, request):
    method patch (line 62) | def patch(self, request):
    method delete (line 81) | def delete(self, request):
  class RoleView (line 100) | class RoleView(AdminView):
    method get (line 101) | def get(self, request):
    method post (line 105) | def post(self, request):
    method patch (line 118) | def patch(self, request):
    method delete (line 140) | def delete(self, request):
  class SelfView (line 152) | class SelfView(View):
    method get (line 153) | def get(self, request):
    method patch (line 157) | def patch(self, request):
  function login (line 187) | def login(request):
  function handle_login_record (line 227) | def handle_login_record(request, username, login_type, error=None):
  function handle_user_info (line 242) | def handle_user_info(handle_response, request, user, captcha):
  function logout (line 286) | def logout(request):

FILE: spug_api/apps/alarm/models.py
  class Alarm (line 10) | class Alarm(models.Model, ModelMixin):
    method to_dict (line 33) | def to_dict(self, *args, **kwargs):
    method __repr__ (line 40) | def __repr__(self):
    class Meta (line 43) | class Meta:
  class Group (line 48) | class Group(models.Model, ModelMixin):
    method to_dict (line 55) | def to_dict(self, *args, **kwargs):
    method __repr__ (line 60) | def __repr__(self):
    class Meta (line 63) | class Meta:
  class Contact (line 68) | class Contact(models.Model, ModelMixin):
    method __repr__ (line 81) | def __repr__(self):
    class Meta (line 84) | class Meta:

FILE: spug_api/apps/alarm/views.py
  class AlarmView (line 14) | class AlarmView(View):
    method get (line 16) | def get(self, request):
  class GroupView (line 21) | class GroupView(View):
    method get (line 23) | def get(self, request):
    method post (line 28) | def post(self, request):
    method delete (line 45) | def delete(self, request):
  class ContactView (line 57) | class ContactView(View):
    method get (line 59) | def get(self, request):
    method post (line 79) | def post(self, request):
    method delete (line 100) | def delete(self, request):
  function handle_test (line 113) | def handle_test(request):

FILE: spug_api/apps/apis/config.py
  function get_configs (line 13) | def get_configs(request):
  function _kv_response (line 30) | def _kv_response(data):
  function _env_response (line 37) | def _env_response(data):
  function _json_response (line 44) | def _json_response(data):
  function _parse_params (line 49) | def _parse_params(request):

FILE: spug_api/apps/apis/deploy.py
  function auto_deploy (line 16) | def auto_deploy(request, deploy_id, kind):
  function _parse_request (line 37) | def _parse_request(request):
  function _parse_message (line 84) | def _parse_message(body, repo):
  function _dispatch (line 96) | def _dispatch(deploy_id, ref, commit_id=None, message=None):

FILE: spug_api/apps/app/models.py
  class App (line 14) | class App(models.Model, ModelMixin):
    method to_dict (line 24) | def to_dict(self, *args, **kwargs):
    method __repr__ (line 30) | def __repr__(self):
    class Meta (line 33) | class Meta:
  class Deploy (line 38) | class Deploy(models.Model, ModelMixin):
    method extend_obj (line 56) | def extend_obj(self):
    method to_dict (line 60) | def to_dict(self, *args, **kwargs):
    method delete (line 69) | def delete(self, using=None, keep_parents=False):
    method __repr__ (line 76) | def __repr__(self):
    class Meta (line 79) | class Meta:
  class DeployExtend1 (line 84) | class DeployExtend1(models.Model, ModelMixin):
    method to_dict (line 96) | def to_dict(self, *args, **kwargs):
    method __repr__ (line 101) | def __repr__(self):
    class Meta (line 104) | class Meta:
  class DeployExtend2 (line 108) | class DeployExtend2(models.Model, ModelMixin):
    method to_dict (line 114) | def to_dict(self, *args, **kwargs):
    method __repr__ (line 120) | def __repr__(self):
    class Meta (line 123) | class Meta:

FILE: spug_api/apps/app/utils.py
  function parse_envs (line 12) | def parse_envs(text):
  function fetch_versions (line 23) | def fetch_versions(deploy: Deploy):
  function fetch_repo (line 31) | def fetch_repo(deploy_id, git_repo):
  function remove_repo (line 38) | def remove_repo(deploy_id):

FILE: spug_api/apps/app/views.py
  class AppView (line 15) | class AppView(View):
    method get (line 16) | def get(self, request):
    method post (line 34) | def post(self, request):
    method patch (line 60) | def patch(self, request):
    method delete (line 87) | def delete(self, request):
  class DeployView (line 107) | class DeployView(View):
    method get (line 109) | def get(self, request):
    method post (line 123) | def post(self, request):
    method delete (line 186) | def delete(self, request):
  function get_versions (line 201) | def get_versions(request, d_id):
  function kit_key (line 212) | def kit_key(request):

FILE: spug_api/apps/config/models.py
  class Environment (line 9) | class Environment(models.Model, ModelMixin):
    method __repr__ (line 17) | def __repr__(self):
    class Meta (line 20) | class Meta:
  class Service (line 25) | class Service(models.Model, ModelMixin):
    method __repr__ (line 32) | def __repr__(self):
    class Meta (line 35) | class Meta:
  class Config (line 40) | class Config(models.Model, ModelMixin):
    method __repr__ (line 55) | def __repr__(self):
    class Meta (line 58) | class Meta:
  class ConfigHistory (line 63) | class ConfigHistory(models.Model, ModelMixin):
    method __repr__ (line 81) | def __repr__(self):
    class Meta (line 84) | class Meta:

FILE: spug_api/apps/config/utils.py
  function compose_configs (line 9) | def compose_configs(app, env_id, no_prefix=False):

FILE: spug_api/apps/config/views.py
  class EnvironmentView (line 14) | class EnvironmentView(View):
    method get (line 15) | def get(self, request):
    method post (line 23) | def post(self, request):
    method patch (line 46) | def patch(self, request):
    method delete (line 67) | def delete(self, request):
  class ServiceView (line 83) | class ServiceView(View):
    method get (line 85) | def get(self, request):
    method post (line 98) | def post(self, request):
    method delete (line 122) | def delete(self, request):
  class ConfigView (line 142) | class ConfigView(View):
    method get (line 144) | def get(self, request):
    method post (line 160) | def post(self, request):
    method patch (line 184) | def patch(self, request):
    method delete (line 211) | def delete(self, request):
  class HistoryView (line 230) | class HistoryView(View):
    method post (line 232) | def post(self, request):
  function post_diff (line 250) | def post_diff(request):
  function parse_json (line 268) | def parse_json(request):
  function parse_text (line 282) | def parse_text(request):
  function _parse (line 303) | def _parse(request, query, data):
  function _filter_value (line 337) | def _filter_value(value):

FILE: spug_api/apps/deploy/helper.py
  class SpugError (line 14) | class SpugError(Exception):
  class Helper (line 18) | class Helper:
    method __init__ (line 19) | def __init__(self, rds, key):
    method make (line 25) | def make(cls, rds, key, host_ids=None):
    method _make_dd_notify (line 45) | def _make_dd_notify(cls, url, action, req, version, host_str):
    method _make_wx_notify (line 95) | def _make_wx_notify(cls, url, action, req, version, host_str):
    method _make_fs_notify (line 142) | def _make_fs_notify(cls, url, action, req, version, host_str):
    method send_deploy_notify (line 191) | def send_deploy_notify(cls, req, action=None):
    method add_callback (line 227) | def add_callback(self, func):
    method parse_filter_rule (line 230) | def parse_filter_rule(self, data: str, sep='\n', env=None):
    method _send (line 239) | def _send(self, message):
    method send_info (line 242) | def send_info(self, key, message):
    method send_error (line 246) | def send_error(self, key, message, with_break=True):
    method send_step (line 252) | def send_step(self, key, step, data):
    method clear (line 255) | def clear(self):
    method progress_callback (line 264) | def progress_callback(self, key):
    method local (line 272) | def local(self, command, env=None):
    method remote (line 292) | def remote(self, key, ssh, command, env=None):
    method remote_raw (line 299) | def remote_raw(self, key, ssh, command):

FILE: spug_api/apps/deploy/models.py
  class DeployRequest (line 14) | class DeployRequest(models.Model, ModelMixin):
    method is_quick_deploy (line 50) | def is_quick_deploy(self):
    method delete (line 56) | def delete(self, using=None, keep_parents=False):
    method __repr__ (line 67) | def __repr__(self):
    class Meta (line 70) | class Meta:

FILE: spug_api/apps/deploy/utils.py
  function dispatch (line 24) | def dispatch(req, fail_mode=False):
  function _ext1_deploy (line 76) | def _ext1_deploy(req, helper, env):
  function _ext2_deploy (line 131) | def _ext2_deploy(req, helper, env):
  function _deploy_ext1_host (line 224) | def _deploy_ext1_host(req, helper, h_id, env):
  function _deploy_ext2_host (line 283) | def _deploy_ext2_host(helper, h_id, actions, env, spug_version):

FILE: spug_api/apps/deploy/views.py
  class RequestView (line 23) | class RequestView(View):
    method get (line 25) | def get(self, request):
    method delete (line 64) | def delete(self, request):
  class RequestDetailView (line 101) | class RequestDetailView(View):
    method get (line 103) | def get(self, request, r_id):
    method post (line 145) | def post(self, request, r_id):
    method patch (line 185) | def patch(self, request, r_id):
  function post_request_ext1 (line 208) | def post_request_ext1(request):
  function post_request_ext1_rollback (line 257) | def post_request_ext1_rollback(request):
  function post_request_ext2 (line 289) | def post_request_ext2(request):
  function get_request_info (line 330) | def get_request_info(request):
  function do_upload (line 344) | def do_upload(request):

FILE: spug_api/apps/exec/executors.py
  function exec_worker_handler (line 13) | def exec_worker_handler(job):
  class Job (line 18) | class Job:
    method __init__ (line 19) | def __init__(self, key, name, hostname, port, username, pkey, command,...
    method _send (line 37) | def _send(self, message):
    method _handle_command (line 40) | def _handle_command(self, command, interpreter):
    method send (line 46) | def send(self, data):
    method send_status (line 49) | def send_status(self, code):
    method run (line 52) | def run(self):

FILE: spug_api/apps/exec/management/commands/runworker.py
  class Worker (line 25) | class Worker:
    method __init__ (line 26) | def __init__(self):
    method job_done (line 30) | def job_done(self, future):
    method queue_monitor (line 33) | def queue_monitor(self):
    method run (line 52) | def run(self):
  class Command (line 70) | class Command(BaseCommand):
    method handle (line 73) | def handle(self, *args, **options):

FILE: spug_api/apps/exec/models.py
  class ExecTemplate (line 10) | class ExecTemplate(models.Model, ModelMixin):
    method __repr__ (line 23) | def __repr__(self):
    method to_view (line 26) | def to_view(self):
    class Meta (line 32) | class Meta:
  class ExecHistory (line 37) | class ExecHistory(models.Model, ModelMixin):
    method to_view (line 47) | def to_view(self):
    class Meta (line 57) | class Meta:
  class Transfer (line 62) | class Transfer(models.Model, ModelMixin):
    method to_view (line 71) | def to_view(self):
    class Meta (line 76) | class Meta:

FILE: spug_api/apps/exec/transfer.py
  class TransferView (line 24) | class TransferView(View):
    method get (line 26) | def get(self, request):
    method post (line 31) | def post(self, request):
    method patch (line 86) | def patch(self, request):
  function _dispatch_sync (line 96) | def _dispatch_sync(task):
  function _do_sync (line 121) | def _do_sync(rds, task, host):

FILE: spug_api/apps/exec/views.py
  class TemplateView (line 15) | class TemplateView(View):
    method get (line 18) | def get(self, request):
    method post (line 24) | def post(self, request):
    method delete (line 46) | def delete(self, request):
  class TaskView (line 55) | class TaskView(View):
    method get (line 57) | def get(self, request):
    method post (line 62) | def post(self, request):
    method patch (line 93) | def patch(self, request):

FILE: spug_api/apps/file/utils.py
  class FileResponseAfter (line 15) | class FileResponseAfter(FileResponse):
    method __init__ (line 16) | def __init__(self, callback, *args, **kwargs):
    method close (line 20) | def close(self):
  function parse_mode (line 25) | def parse_mode(obj):
  function format_size (line 58) | def format_size(size):
  function fetch_dir_list (line 73) | def fetch_dir_list(host, path):

FILE: spug_api/apps/file/views.py
  class FileView (line 14) | class FileView(View):
    method get (line 16) | def get(self, request):
  class ObjectView (line 32) | class ObjectView(View):
    method get (line 34) | def get(self, request):
    method post (line 53) | def post(self, request):
    method delete (line 75) | def delete(self, request):
    method _compute_progress (line 90) | def _compute_progress(self, rds_cli, token, total, value, *args):

FILE: spug_api/apps/home/models.py
  class Notice (line 9) | class Notice(models.Model, ModelMixin):
    method to_view (line 17) | def to_view(self):
    class Meta (line 22) | class Meta:
  class Navigation (line 27) | class Navigation(models.Model, ModelMixin):
    method to_view (line 35) | def to_view(self):
    class Meta (line 40) | class Meta:

FILE: spug_api/apps/home/navigation.py
  class NavView (line 10) | class NavView(View):
    method get (line 11) | def get(self, request):
    method post (line 15) | def post(self, request):
    method patch (line 33) | def patch(self, request):
    method delete (line 53) | def delete(self, request):

FILE: spug_api/apps/home/notice.py
  class NoticeView (line 10) | class NoticeView(View):
    method get (line 11) | def get(self, request):
    method post (line 15) | def post(self, request):
    method patch (line 33) | def patch(self, request):
    method delete (line 58) | def delete(self, request):

FILE: spug_api/apps/home/views.py
  function get_statistic (line 19) | def get_statistic(request):
  function get_alarm (line 37) | def get_alarm(request):
  function get_request (line 55) | def get_request(request):
  function get_deploy (line 71) | def get_deploy(request):

FILE: spug_api/apps/host/add.py
  function get_regions (line 11) | def get_regions(request):
  function cloud_import (line 30) | def cloud_import(request):

FILE: spug_api/apps/host/extend.py
  class ExtendView (line 11) | class ExtendView(View):
    method get (line 13) | def get(self, request):
    method post (line 29) | def post(self, request):

FILE: spug_api/apps/host/group.py
  function fetch_children (line 11) | def fetch_children(data, with_hosts):
  function merge_children (line 21) | def merge_children(data, prefix, childes):
  function filter_by_perm (line 32) | def filter_by_perm(data, result, ids):
  class GroupView (line 41) | class GroupView(View):
    method get (line 42) | def get(self, request):
    method post (line 60) | def post(self, request):
    method patch (line 76) | def patch(self, request):
    method delete (line 106) | def delete(self, request):

FILE: spug_api/apps/host/models.py
  class Host (line 12) | class Host(models.Model, ModelMixin):
    method private_key (line 24) | def private_key(self):
    method get_ssh (line 27) | def get_ssh(self, pkey=None, default_env=None):
    method to_view (line 31) | def to_view(self):
    method __repr__ (line 38) | def __repr__(self):
    class Meta (line 41) | class Meta:
  class HostExtend (line 46) | class HostExtend(models.Model, ModelMixin):
    method to_view (line 73) | def to_view(self):
    class Meta (line 82) | class Meta:
  class Group (line 86) | class Group(models.Model, ModelMixin):
    method to_view (line 92) | def to_view(self, with_hosts=False):
    class Meta (line 101) | class Meta:

FILE: spug_api/apps/host/utils.py
  function check_os_type (line 20) | def check_os_type(os_name):
  function check_instance_charge_type (line 29) | def check_instance_charge_type(value, supplier):
  function check_internet_charge_type (line 43) | def check_internet_charge_type(value, supplier):
  function parse_utc_date (line 57) | def parse_utc_date(value):
  function fetch_ali_regions (line 67) | def fetch_ali_regions(ak, ac):
  function fetch_ali_disks (line 76) | def fetch_ali_disks(ak, ac, region_id, page_number=1):
  function fetch_ali_instances (line 97) | def fetch_ali_instances(ak, ac, region_id, page_number=1):
  function fetch_tencent_regions (line 139) | def fetch_tencent_regions(ak, ac):
  function fetch_tencent_instances (line 148) | def fetch_tencent_instances(ak, ac, region_id, page_number=1):
  function fetch_host_extend (line 185) | def fetch_host_extend(ssh):
  function batch_sync_host (line 252) | def batch_sync_host(token, hosts, password=None):
  function _sync_host_extend (line 274) | def _sync_host_extend(host, private_key=None, public_key=None, password=...
  function _get_ssh (line 293) | def _get_ssh(kwargs, pkey=None, private_key=None, public_key=None, passw...

FILE: spug_api/apps/host/views.py
  class HostView (line 24) | class HostView(View):
    method get (line 25) | def get(self, request):
    method post (line 35) | def post(self, request):
    method put (line 67) | def put(self, request):
    method patch (line 78) | def patch(self, request):
    method delete (line 96) | def delete(self, request):
  function post_import (line 129) | def post_import(request):
  function post_parse (line 169) | def post_parse(request):
  function batch_valid (line 179) | def batch_valid(request):
  function _do_host_verify (line 195) | def _do_host_verify(form):

FILE: spug_api/apps/monitor/executors.py
  function site_check (line 20) | def site_check(url, limit):
  function port_check (line 36) | def port_check(addr, port):
  function ping_check (line 47) | def ping_check(addr):
  function host_executor (line 62) | def host_executor(host, command):
  function monitor_worker_handler (line 74) | def monitor_worker_handler(job):
  function dispatch (line 111) | def dispatch(tp, addr, extra):

FILE: spug_api/apps/monitor/management/commands/runmonitor.py
  class Command (line 11) | class Command(BaseCommand):
    method handle (line 14) | def handle(self, *args, **options):

FILE: spug_api/apps/monitor/models.py
  class Detection (line 10) | class Detection(models.Model, ModelMixin):
    method to_view (line 42) | def to_view(self):
    method __repr__ (line 50) | def __repr__(self):
    class Meta (line 53) | class Meta:

FILE: spug_api/apps/monitor/scheduler.py
  class Scheduler (line 21) | class Scheduler:
    method __init__ (line 24) | def __init__(self):
    method _dispatch (line 27) | def _dispatch(self, task_id, tp, targets, extra, threshold, quiet):
    method _init (line 34) | def _init(self):
    method run (line 51) | def run(self):

FILE: spug_api/apps/monitor/utils.py
  function seconds_to_human (line 11) | def seconds_to_human(seconds):
  function _record_alarm (line 24) | def _record_alarm(det, target, duration, status):
  function handle_notify (line 35) | def handle_notify(task_id, target, is_ok, out, fault_times):

FILE: spug_api/apps/monitor/views.py
  class DetectionView (line 15) | class DetectionView(View):
    method get (line 17) | def get(self, request):
    method post (line 23) | def post(self, request):
    method patch (line 65) | def patch(self, request):
    method delete (line 84) | def delete(self, request):
  function run_test (line 98) | def run_test(request):
  function get_overview (line 111) | def get_overview(request):

FILE: spug_api/apps/notify/models.py
  class Notify (line 11) | class Notify(models.Model, ModelMixin):
    method make_system_notify (line 32) | def make_system_notify(cls, title, content):
    method make_monitor_notify (line 36) | def make_monitor_notify(cls, title, content):
    method make_schedule_notify (line 40) | def make_schedule_notify(cls, title, content):
    method make_deploy_notify (line 44) | def make_deploy_notify(cls, title, content):
    method _make_notify (line 48) | def _make_notify(cls, source, type, title, content):
    method __repr__ (line 57) | def __repr__(self):
    class Meta (line 60) | class Meta:

FILE: spug_api/apps/notify/views.py
  class NotifyView (line 9) | class NotifyView(View):
    method get (line 10) | def get(self, request):
    method patch (line 14) | def patch(self, request):

FILE: spug_api/apps/repository/models.py
  class Repository (line 14) | class Repository(models.Model, ModelMixin):
    method make_spug_version (line 33) | def make_spug_version(deploy_id):
    method to_view (line 36) | def to_view(self):
    method delete (line 48) | def delete(self, using=None, keep_parents=False):
    class Meta (line 56) | class Meta:

FILE: spug_api/apps/repository/utils.py
  function dispatch (line 20) | def dispatch(rep: Repository, helper=None):
  function _build (line 66) | def _build(rep: Repository, helper, env):

FILE: spug_api/apps/repository/views.py
  class RepositoryView (line 17) | class RepositoryView(View):
    method get (line 19) | def get(self, request):
    method post (line 41) | def post(self, request):
    method patch (line 64) | def patch(self, request):
    method delete (line 79) | def delete(self, request):
  function get_requests (line 94) | def get_requests(request):
  function get_detail (line 109) | def get_detail(request, r_id):

FILE: spug_api/apps/schedule/builtin.py
  function auto_run_by_day (line 24) | def auto_run_by_day():
  function auto_run_by_minute (line 69) | def auto_run_by_minute():

FILE: spug_api/apps/schedule/executors.py
  function local_executor (line 15) | def local_executor(command):
  function host_executor (line 28) | def host_executor(host, command):
  function dispatch_job (line 40) | def dispatch_job(host_id, interpreter, command):
  function schedule_worker_handler (line 55) | def schedule_worker_handler(job):

FILE: spug_api/apps/schedule/management/commands/runscheduler.py
  class Command (line 11) | class Command(BaseCommand):
    method handle (line 14) | def handle(self, *args, **options):

FILE: spug_api/apps/schedule/models.py
  class History (line 10) | class History(models.Model, ModelMixin):
    method to_list (line 21) | def to_list(self):
    class Meta (line 26) | class Meta:
  class Task (line 31) | class Task(models.Model, ModelMixin):
    method to_dict (line 55) | def to_dict(self, *args, **kwargs):
    method __repr__ (line 66) | def __repr__(self):
    class Meta (line 69) | class Meta:

FILE: spug_api/apps/schedule/scheduler.py
  class Scheduler (line 22) | class Scheduler:
    method __init__ (line 39) | def __init__(self):
    method covert_week (line 43) | def covert_week(cls, week_str):
    method parse_trigger (line 47) | def parse_trigger(cls, trigger, trigger_args):
    method _init_builtin_jobs (line 61) | def _init_builtin_jobs(self):
    method _dispatch (line 65) | def _dispatch(self, task_id, interpreter, command, targets):
    method _init (line 79) | def _init(self):
    method run (line 95) | def run(self):

FILE: spug_api/apps/schedule/utils.py
  function send_fail_notify (line 11) | def send_fail_notify(task, msg=None):
  function _do_notify (line 19) | def _do_notify(task, mode, url, msg):

FILE: spug_api/apps/schedule/views.py
  class Schedule (line 17) | class Schedule(View):
    method get (line 19) | def get(self, request):
    method post (line 25) | def post(self, request):
    method patch (line 68) | def patch(self, request):
    method delete (line 89) | def delete(self, request):
  class HistoryView (line 103) | class HistoryView(View):
    method get (line 105) | def get(self, request, t_id):
    method post (line 118) | def post(self, request, t_id):
    method _fetch_detail (line 137) | def _fetch_detail(self, h_id):
  function next_run_time (line 160) | def next_run_time(request):

FILE: spug_api/apps/setting/models.py
  class Setting (line 23) | class Setting(models.Model, ModelMixin):
    method to_view (line 28) | def to_view(self):
    method real_val (line 34) | def real_val(self):
    method __repr__ (line 40) | def __repr__(self):
    class Meta (line 43) | class Meta:
  class UserSetting (line 47) | class UserSetting(models.Model, ModelMixin):
    class Meta (line 52) | class Meta:

FILE: spug_api/apps/setting/user.py
  class UserSettingView (line 9) | class UserSettingView(View):
    method get (line 10) | def get(self, request):
    method post (line 16) | def post(self, request):

FILE: spug_api/apps/setting/utils.py
  class AppSetting (line 10) | class AppSetting:
    method get (line 13) | def get(cls, key):
    method get_default (line 20) | def get_default(cls, key, default=None):
    method set (line 27) | def set(cls, key, value, desc=None):
    method delete (line 35) | def delete(cls, key):
    method get_ssh_key (line 39) | def get_ssh_key(cls):

FILE: spug_api/apps/setting/views.py
  class SettingView (line 19) | class SettingView(AdminView):
    method get (line 20) | def get(self, request):
    method post (line 29) | def post(self, request):
  class MFAView (line 39) | class MFAView(AdminView):
    method get (line 40) | def get(self, request):
    method post (line 52) | def post(self, request):
  function ldap_test (line 75) | def ldap_test(request):
  function email_test (line 94) | def email_test(request):
  function get_about (line 113) | def get_about(request):
  function handle_push_bind (line 123) | def handle_push_bind(request):
  function handle_push_balance (line 143) | def handle_push_balance(request):

FILE: spug_api/consumer/consumers.py
  class ComConsumer (line 16) | class ComConsumer(BaseConsumer):
    method __init__ (line 17) | def __init__(self, *args, **kwargs):
    method disconnect (line 31) | def disconnect(self, code):
    method get_response (line 34) | def get_response(self, index):
    method receive (line 43) | def receive(self, text_data='', **kwargs):
  class SSHConsumer (line 54) | class SSHConsumer(BaseConsumer):
    method __init__ (line 55) | def __init__(self, *args, **kwargs):
    method loop_read (line 61) | def loop_read(self):
    method receive (line 86) | def receive(self, text_data=None, bytes_data=None):
    method disconnect (line 97) | def disconnect(self, code):
    method init (line 103) | def init(self):
  class NotifyConsumer (line 122) | class NotifyConsumer(BaseConsumer):
    method init (line 123) | def init(self):
    method disconnect (line 126) | def disconnect(self, code):
    method receive (line 129) | def receive(self, **kwargs):
    method notify_message (line 132) | def notify_message(self, event):
  class PubSubConsumer (line 136) | class PubSubConsumer(BaseConsumer):
    method __init__ (line 137) | def __init__(self, *args, **kwargs):
    method disconnect (line 144) | def disconnect(self, code):
    method receive (line 148) | def receive(self, **kwargs):

FILE: spug_api/consumer/utils.py
  function get_real_ip (line 13) | def get_real_ip(headers):
  class BaseConsumer (line 18) | class BaseConsumer(WebsocketConsumer):
    method __init__ (line 19) | def __init__(self, *args, **kwargs):
    method close_with_message (line 23) | def close_with_message(self, content):
    method connect (line 27) | def connect(self):

FILE: spug_api/libs/channel.py
  class Channel (line 11) | class Channel:
    method get_token (line 13) | def get_token():
    method send_notify (line 17) | def send_notify(title, content):

FILE: spug_api/libs/decorators.py
  function auth (line 8) | def auth(perm_list):

FILE: spug_api/libs/gitlib.py
  class Git (line 11) | class Git:
    method __init__ (line 12) | def __init__(self, git_repo, repo_dir, pkey=None):
    method archive (line 20) | def archive(self, filepath, commit):
    method fetch_branches_tags (line 24) | def fetch_branches_tags(self):
    method fetch (line 46) | def fetch(self):
    method _get_repo (line 58) | def _get_repo(self):
    method _get_commits (line 76) | def _get_commits(self, branch, count=10):
    method _format_date (line 89) | def _format_date(self, timestamp):
    method __enter__ (line 95) | def __enter__(self):
    method __exit__ (line 104) | def __exit__(self, exc_type, exc_val, exc_tb):

FILE: spug_api/libs/helper.py
  function _special_url_encode (line 16) | def _special_url_encode(value) -> str:
  function _make_ali_signature (line 24) | def _make_ali_signature(key: str, params: dict) -> bytes:
  function _make_tencent_signature (line 31) | def _make_tencent_signature(endpoint: str, key: str, params: dict) -> by...
  function make_ali_request (line 38) | def make_ali_request(ak, ac, endpoint, params):
  function make_tencent_request (line 52) | def make_tencent_request(ak, ac, endpoint, params):

FILE: spug_api/libs/ldap.py
  class LDAP (line 7) | class LDAP:
    method __init__ (line 8) | def __init__(self, server, port, rules, admin_dn, password, base_dn):
    method valid_user (line 16) | def valid_user(self, username, password):

FILE: spug_api/libs/mail.py
  class Mail (line 7) | class Mail:
    method __init__ (line 8) | def __init__(self, server, port, username, password, nickname=None):
    method get_server (line 15) | def get_server(self):
    method send_text_mail (line 27) | def send_text_mail(self, receivers, subject, body):

FILE: spug_api/libs/middleware.py
  class HandleExceptionMiddleware (line 13) | class HandleExceptionMiddleware(MiddlewareMixin):
    method process_exception (line 18) | def process_exception(self, request, exception):
  class AuthenticationMiddleware (line 23) | class AuthenticationMiddleware(MiddlewareMixin):
    method process_request (line 28) | def process_request(self, request):

FILE: spug_api/libs/mixins.py
  class ModelMixin (line 9) | class ModelMixin(object):
    method to_dict (line 12) | def to_dict(self, excludes: tuple = None, selects: tuple = None) -> dict:
    method update_by_dict (line 22) | def update_by_dict(self, data):
  class AdminView (line 28) | class AdminView(View):
    method dispatch (line 29) | def dispatch(self, request, *args, **kwargs):

FILE: spug_api/libs/parser.py
  class ParseError (line 10) | class ParseError(BaseException):
    method __init__ (line 11) | def __init__(self, message):
  class Argument (line 16) | class Argument(object):
    method __init__ (line 23) | def __init__(self, name, default=None, handler=None, required=True, ty...
    method parse (line 36) | def parse(self, has_key, value):
  class BaseParser (line 76) | class BaseParser(object):
    method __init__ (line 77) | def __init__(self, *args):
    method _get (line 86) | def _get(self, key):
    method _init (line 89) | def _init(self, data):
    method add_argument (line 92) | def add_argument(self, **kwargs):
    method parse (line 95) | def parse(self, data=None, clear=False):
  class JsonParser (line 110) | class JsonParser(BaseParser):
    method __init__ (line 111) | def __init__(self, *args):
    method _get (line 115) | def _get(self, key):
    method _init (line 118) | def _init(self, data):

FILE: spug_api/libs/push.py
  function get_balance (line 10) | def get_balance(token):
  function get_contacts (line 20) | def get_contacts(token):
  function send_login_code (line 30) | def send_login_code(token, user, code):

FILE: spug_api/libs/spug.py
  function _gen_dd_sign (line 19) | def _gen_dd_sign(secret):
  function _gen_fs_sign (line 27) | def _gen_fs_sign(secret):
  class Notification (line 35) | class Notification:
    method __init__ (line 36) | def __init__(self, grp, event, target, title, message, duration):
    method handle_request (line 46) | def handle_request(url, data, mode=None):
    method monitor_by_email (line 70) | def monitor_by_email(self, users):
    method monitor_by_dd (line 91) | def monitor_by_dd(self, users):
    method monitor_by_fs (line 117) | def monitor_by_fs(self, users):
    method monitor_by_qy_wx (line 146) | def monitor_by_qy_wx(self, users):
    method monitor_by_spug_push (line 166) | def monitor_by_spug_push(self, targets):
    method dispatch_monitor (line 187) | def dispatch_monitor(self, modes):

FILE: spug_api/libs/ssh.py
  function _finalize_pubkey_algorithm (line 15) | def _finalize_pubkey_algorithm(self, key_type):
  class SSH (line 52) | class SSH:
    method __init__ (line 53) | def __init__(self, hostname, port=22, username='root', pkey=None, pass...
    method generate_key (line 77) | def generate_key():
    method get_client (line 83) | def get_client(self):
    method ping (line 91) | def ping(self):
    method add_public_key (line 94) | def add_public_key(self, public_key):
    method exec_command_raw (line 102) | def exec_command_raw(self, command, environment=None):
    method exec_command (line 111) | def exec_command(self, command, environment=None):
    method _win_exec_command_with_stream (line 126) | def _win_exec_command_with_stream(self, command, environment=None):
    method exec_command_with_stream (line 140) | def exec_command_with_stream(self, command, environment=None):
    method put_file (line 157) | def put_file(self, local_path, remote_path, callback=None):
    method put_file_by_fl (line 161) | def put_file_by_fl(self, fl, remote_path, callback=None):
    method list_dir_attr (line 165) | def list_dir_attr(self, path):
    method sftp_stat (line 169) | def sftp_stat(self, path):
    method remove_file (line 173) | def remove_file(self, path):
    method _get_channel (line 177) | def _get_channel(self):
    method _get_sftp (line 203) | def _get_sftp(self):
    method _make_env_command (line 210) | def _make_env_command(self, environment):
    method _handle_command (line 222) | def _handle_command(self, command, environment):
    method _decode (line 237) | def _decode(self, content):
    method __enter__ (line 244) | def __enter__(self):
    method __exit__ (line 252) | def __exit__(self, exc_type, exc_val, exc_tb):

FILE: spug_api/libs/utils.py
  function human_datetime (line 15) | def human_datetime(date=None):
  function human_date (line 24) | def human_date(date=None):
  function human_time (line 32) | def human_time(date=None):
  function str_decode (line 40) | def str_decode(data):
  function parse_time (line 52) | def parse_time(value):
  function human_seconds_time (line 64) | def human_seconds_time(seconds):
  function render_str (line 81) | def render_str(template, datasheet):
  function json_response (line 85) | def json_response(data='', error=''):
  class AttrDict (line 97) | class AttrDict(dict):
    method __setattr__ (line 98) | def __setattr__(self, key, value):
    method __getattr__ (line 101) | def __getattr__(self, item):
    method __delattr__ (line 107) | def __delattr__(self, item):
  class DateTimeEncoder (line 112) | class DateTimeEncoder(json.JSONEncoder):
    method default (line 113) | def default(self, o):
  function generate_random_str (line 125) | def generate_random_str(length: int = 4, is_digits: bool = True) -> str:
  function get_request_real_ip (line 130) | def get_request_real_ip(headers: dict):

FILE: spug_api/libs/validators.py
  function ip_validator (line 9) | def ip_validator(value):
  function date_validator (line 18) | def date_validator(value: str) -> bool:

FILE: spug_api/manage.py
  function main (line 10) | def main():

FILE: spug_api/tools/migrate.py
  class Version (line 19) | class Version:
    method __init__ (line 20) | def __init__(self, version):
    method __gt__ (line 23) | def __gt__(self, other):

FILE: spug_web/src/App.js
  class App (line 12) | class App extends Component {
    method render (line 13) | render() {

FILE: spug_web/src/components/Action.js
  function canVisible (line 11) | function canVisible(auth) {
  class Action (line 15) | class Action extends React.Component {
    method Link (line 16) | static Link(props) {
    method Button (line 20) | static Button(props) {
    method render (line 32) | render() {

FILE: spug_web/src/components/AppSelector.js
  function _initEnv (line 34) | function _initEnv() {

FILE: spug_web/src/components/AuthButton.js
  function AuthButton (line 11) | function AuthButton(props) {

FILE: spug_web/src/components/AuthCard.js
  function AuthCard (line 11) | function AuthCard(props) {

FILE: spug_web/src/components/AuthDiv.js
  function AuthDiv (line 10) | function AuthDiv(props) {

FILE: spug_web/src/components/AuthFragment.js
  function AuthFragment (line 10) | function AuthFragment(props) {

FILE: spug_web/src/components/Breadcrumb.js
  method render (line 14) | render() {

FILE: spug_web/src/components/Link.js
  function Link (line 9) | function Link(props) {

FILE: spug_web/src/components/LinkButton.js
  function LinkButton (line 11) | function LinkButton(props) {

FILE: spug_web/src/components/NotFound.js
  function NotFound (line 4) | function NotFound() {

FILE: spug_web/src/components/SearchForm.js
  method Item (line 11) | static Item(props) {
  method render (line 21) | render() {

FILE: spug_web/src/components/StatisticsCard.js
  class StatisticsCard (line 12) | class StatisticsCard extends React.Component {
    method render (line 23) | render() {

FILE: spug_web/src/components/TableCard.js
  function Search (line 14) | function Search(props) {
  function Footer (line 35) | function Footer(props) {
  function Header (line 50) | function Header(props) {
  function TableCard (line 124) | function TableCard(props) {

FILE: spug_web/src/gStore.js
  class Store (line 10) | class Store {

FILE: spug_web/src/layout/Header.js
  function handleLogout (line 19) | function handleLogout() {
  function openTerminal (line 24) | function openTerminal() {

FILE: spug_web/src/layout/Notification.js
  function Icon (line 18) | function Icon(props) {
  function fetch (line 55) | function fetch() {
  function listen (line 65) | function listen() {
  function handleVisible (line 86) | function handleVisible(visible) {
  function handleRead (line 92) | function handleRead(e, item) {
  function handleReadAll (line 101) | function handleReadAll() {

FILE: spug_web/src/layout/Sider.js
  function Sider (line 20) | function Sider(props) {

FILE: spug_web/src/layout/index.js
  function initRoutes (line 17) | function initRoutes(Routes, routes) {

FILE: spug_web/src/libs/functools.js
  constant X_TOKEN (line 12) | let X_TOKEN;
  function updatePermissions (line 15) | function updatePermissions() {
  function hasPermission (line 27) | function hasPermission(strCode) {
  function clsNames (line 38) | function clsNames(...args) {
  function isInclude (line 42) | function isInclude(s, keys) {
  function includes (line 57) | function includes(s, keys) {
  function cleanCommand (line 69) | function cleanCommand(text) {
  function isSubArray (line 74) | function isSubArray(parent, child) {
  function trimFixed (line 84) | function trimFixed(data, bit) {
  function human_date (line 89) | function human_date(date) {
  function human_time (line 97) | function human_time(date) {
  function human_datetime (line 105) | function human_datetime(date) {
  function uniqueId (line 110) | function uniqueId() {

FILE: spug_web/src/libs/http.js
  function handleResponse (line 12) | function handleResponse(response) {

FILE: spug_web/src/libs/index.js
  constant VERSION (line 13) | const VERSION = 'v3.3.3';

FILE: spug_web/src/libs/router.js
  function makeRoute (line 13) | function makeRoute(path, component) {
  function makeModuleRoute (line 18) | function makeModuleRoute(prefix, routes) {
  function NotFound (line 23) | function NotFound() {
  class Router (line 38) | class Router extends React.Component {
    method constructor (line 39) | constructor(props) {
    method initialRoutes (line 45) | initialRoutes() {
    method render (line 54) | render() {

FILE: spug_web/src/pages/alarm/alarm/Table.js
  class ComTable (line 14) | @observer
    method constructor (line 16) | constructor(props) {
    method componentDidMount (line 23) | componentDidMount() {
    method render (line 68) | render() {

FILE: spug_web/src/pages/alarm/alarm/store.js
  class Store (line 9) | class Store {
    method dataSource (line 16) | @computed get dataSource() {

FILE: spug_web/src/pages/alarm/contact/Form.js
  function handleChannelToggle (line 61) | function handleChannelToggle(key, checked) {
  function handleSubmit (line 72) | function handleSubmit() {
  function handleTest (line 90) | function handleTest(mode, name) {
  function Test (line 101) | function Test(props) {

FILE: spug_web/src/pages/alarm/contact/Table.js
  class ComTable (line 14) | @observer
    method componentDidMount (line 16) | componentDidMount() {
    method render (line 34) | render() {

FILE: spug_web/src/pages/alarm/contact/store.js
  class Store (line 9) | class Store {
    method dataSource (line 17) | @computed get dataSource() {

FILE: spug_web/src/pages/alarm/group/Form.js
  function handleSubmit (line 25) | function handleSubmit() {

FILE: spug_web/src/pages/alarm/group/Table.js
  class ComTable (line 15) | @observer
    method constructor (line 17) | constructor(props) {
    method componentDidMount (line 24) | componentDidMount() {
    method render (line 55) | render() {

FILE: spug_web/src/pages/alarm/group/store.js
  class Store (line 9) | class Store {
    method dataSource (line 17) | @computed get dataSource() {

FILE: spug_web/src/pages/config/app/Form.js
  function handleSubmit (line 16) | function handleSubmit() {

FILE: spug_web/src/pages/config/app/Rel.js
  class Rel (line 13) | @observer
    method constructor (line 15) | constructor(props) {
    method componentDidMount (line 24) | componentDidMount() {
    method render (line 50) | render() {

FILE: spug_web/src/pages/config/app/Table.js
  class ComTable (line 14) | @observer
    method componentDidMount (line 16) | componentDidMount() {
    method render (line 39) | render() {

FILE: spug_web/src/pages/config/app/store.js
  class Store (line 9) | class Store {

FILE: spug_web/src/pages/config/environment/Form.js
  function handleSubmit (line 16) | function handleSubmit() {

FILE: spug_web/src/pages/config/environment/Table.js
  function ComTable (line 14) | function ComTable() {

FILE: spug_web/src/pages/config/environment/store.js
  class Store (line 9) | class Store {
    method dataSource (line 18) | @computed get dataSource() {

FILE: spug_web/src/pages/config/service/Form.js
  function handleSubmit (line 16) | function handleSubmit() {

FILE: spug_web/src/pages/config/service/Table.js
  class ComTable (line 14) | @observer
    method componentDidMount (line 16) | componentDidMount() {
    method render (line 39) | render() {

FILE: spug_web/src/pages/config/service/store.js
  class Store (line 9) | class Store {
    method dataSource (line 17) | @computed get dataSource() {

FILE: spug_web/src/pages/config/setting/DiffConfig.js
  class Record (line 15) | @observer
    method constructor (line 17) | constructor(props) {
    method render (line 54) | render() {

FILE: spug_web/src/pages/config/setting/Form.js
  function handleSubmit (line 21) | function handleSubmit() {
  function handleEnvCheck (line 42) | function handleEnvCheck(id) {

FILE: spug_web/src/pages/config/setting/JSONView.js
  class JSONView (line 14) | @observer
    method constructor (line 16) | constructor(props) {
    method componentDidMount (line 25) | componentDidMount() {
    method render (line 53) | render() {

FILE: spug_web/src/pages/config/setting/Record.js
  class Record (line 12) | @observer
    method constructor (line 14) | constructor(props) {
    method componentDidMount (line 23) | componentDidMount() {
    method render (line 57) | render() {

FILE: spug_web/src/pages/config/setting/TableView.js
  class TableView (line 15) | @observer
    method render (line 72) | render() {

FILE: spug_web/src/pages/config/setting/TextView.js
  class TextView (line 17) | @observer
    method constructor (line 19) | constructor(props) {
    method componentDidMount (line 28) | componentDidMount() {
    method render (line 51) | render() {

FILE: spug_web/src/pages/config/setting/index.js
  class Index (line 28) | @observer
    method constructor (line 30) | constructor(props) {
    method componentDidMount (line 39) | componentDidMount() {
    method render (line 72) | render() {

FILE: spug_web/src/pages/config/setting/store.js
  class Store (line 9) | class Store {

FILE: spug_web/src/pages/dashboard/AlarmTrend.js
  function handleChange (line 38) | function handleChange(v) {

FILE: spug_web/src/pages/dashboard/RequestTop.js
  function handleClick (line 28) | function handleClick(val) {

FILE: spug_web/src/pages/dashboard/StatisticCard.js
  class StatisticCard (line 10) | class StatisticCard extends React.Component {
    method constructor (line 11) | constructor(props) {
    method componentDidMount (line 19) | componentDidMount() {
    method render (line 25) | render() {

FILE: spug_web/src/pages/dashboard/index.js
  class HomeIndex (line 12) | class HomeIndex extends React.Component {
    method render (line 13) | render() {

FILE: spug_web/src/pages/deploy/app/AddSelect.js
  class AddSelect (line 13) | @observer
    method render (line 39) | render() {

FILE: spug_web/src/pages/deploy/app/AutoDeploy.js
  function fetchVersions (line 35) | function fetchVersions() {
  function copyToClipBoard (line 42) | function copyToClipBoard(data) {

FILE: spug_web/src/pages/deploy/app/CloneConfirm.js
  function handleChange (line 30) | function handleChange(deployId) {

FILE: spug_web/src/pages/deploy/app/Ext1Setup1.js
  function updateEnvs (line 19) | function updateEnvs() {

FILE: spug_web/src/pages/deploy/app/Ext1Setup2.js
  function handleNext (line 19) | function handleNext() {

FILE: spug_web/src/pages/deploy/app/Ext1Setup3.js
  function handleSubmit (line 17) | function handleSubmit() {

FILE: spug_web/src/pages/deploy/app/Ext2Setup1.js
  function updateEnvs (line 17) | function updateEnvs() {

FILE: spug_web/src/pages/deploy/app/Ext2Setup2.js
  class Ext2Setup2 (line 17) | @observer
    method constructor (line 19) | constructor(props) {
    method render (line 68) | render() {

FILE: spug_web/src/pages/deploy/app/Form.js
  function handleSubmit (line 16) | function handleSubmit() {

FILE: spug_web/src/pages/deploy/app/Repo.js
  function Repo (line 10) | function Repo(props) {

FILE: spug_web/src/pages/deploy/app/Table.js
  function ComTable (line 24) | function ComTable() {

FILE: spug_web/src/pages/deploy/app/store.js
  class Store (line 10) | class Store {
    method dataSource (line 27) | @computed get dataSource() {
    method currentRecord (line 34) | @computed get currentRecord() {

FILE: spug_web/src/pages/deploy/repository/Console.js
  function _makeSocket (line 42) | function _makeSocket(index = 0) {
  function initialTerm (line 71) | function initialTerm() {
  function handleClose (line 88) | function handleClose() {
  function StepItem (line 93) | function StepItem(props) {

FILE: spug_web/src/pages/deploy/repository/Detail.js
  function handleDelete (line 21) | function handleDelete() {

FILE: spug_web/src/pages/deploy/repository/Form.js
  function _setDefault (line 29) | function _setDefault(type, new_extra, new_versions) {
  function _initial (line 49) | function _initial(versions) {
  function fetchVersions (line 68) | function fetchVersions() {
  function switchType (line 78) | function switchType(v) {
  function switchExtra1 (line 83) | function switchExtra1(v) {
  function handleSubmit (line 90) | function handleSubmit() {

FILE: spug_web/src/pages/deploy/repository/Table.js
  function ComTable (line 14) | function ComTable() {

FILE: spug_web/src/pages/deploy/repository/store.js
  class Store (line 9) | class Store {
    method dataSource (line 22) | @computed get dataSource() {

FILE: spug_web/src/pages/deploy/request/Approve.js
  function handleSubmit (line 18) | function handleSubmit() {
  function handleChange (line 29) | function handleChange(val) {

FILE: spug_web/src/pages/deploy/request/BatchDelete.js
  function handleSubmit (line 26) | function handleSubmit() {
  function handleChange (line 46) | function handleChange(e) {

FILE: spug_web/src/pages/deploy/request/Ext1Console.js
  function Ext1Console (line 15) | function Ext1Console(props) {

FILE: spug_web/src/pages/deploy/request/Ext1Form.js
  function NoVersions (line 16) | function NoVersions() {
  function fetchVersions (line 50) | function fetchVersions() {
  function handleSubmit (line 64) | function handleSubmit() {
  function _setDefault (line 84) | function _setDefault(type, new_extra, new_versions, new_repositories) {
  function _initial (line 108) | function _initial(versions, repositories) {
  function switchType (line 127) | function switchType(v) {
  function switchExtra1 (line 132) | function switchExtra1(v) {

FILE: spug_web/src/pages/deploy/request/Ext2Console.js
  function Ext2Console (line 15) | function Ext2Console(props) {

FILE: spug_web/src/pages/deploy/request/Ext2Form.js
  function handleSubmit (line 31) | function handleSubmit() {
  function handleUploadChange (line 51) | function handleUploadChange(v) {
  function handleUpload (line 57) | function handleUpload(file, fileList) {

FILE: spug_web/src/pages/deploy/request/HostSelector.js
  function handleClickRow (line 19) | function handleClickRow(record) {
  function handleSubmit (line 29) | function handleSubmit() {

FILE: spug_web/src/pages/deploy/request/OutView.js
  function OutView (line 10) | function OutView(props) {

FILE: spug_web/src/pages/deploy/request/Rollback.js
  function handleSubmit (line 27) | function handleSubmit() {

FILE: spug_web/src/pages/deploy/request/Table.js
  function DeployConfirm (line 16) | function DeployConfirm() {
  function ComTable (line 26) | function ComTable() {

FILE: spug_web/src/pages/deploy/request/index.js
  function Index (line 26) | function Index() {

FILE: spug_web/src/pages/deploy/request/store.js
  class Store (line 11) | class Store {
    method dataSource (line 30) | @computed get dataSource() {

FILE: spug_web/src/pages/exec/task/Output.js
  function OutView (line 25) | function OutView(props) {

FILE: spug_web/src/pages/exec/task/Parameter.js
  function Render (line 10) | function Render(props) {
  function Parameter (line 32) | function Parameter(props) {

FILE: spug_web/src/pages/exec/task/TemplateSelector.js
  class TemplateSelector (line 13) | @observer
    method constructor (line 15) | constructor(props) {
    method componentDidMount (line 23) | componentDidMount() {
    method render (line 63) | render() {

FILE: spug_web/src/pages/exec/task/index.js
  function TaskIndex (line 21) | function TaskIndex() {

FILE: spug_web/src/pages/exec/task/store.js
  class Store (line 9) | class Store {
    method items (line 17) | @computed get items() {
    method counter (line 30) | @computed get counter() {

FILE: spug_web/src/pages/exec/template/Form.js
  function handleSubmit (line 28) | function handleSubmit() {
  function handleAddZone (line 43) | function handleAddZone() {
  function updateParameter (line 64) | function updateParameter(data) {
  function delParameter (line 76) | function delParameter(index) {

FILE: spug_web/src/pages/exec/template/Parameter.js
  function Parameter (line 11) | function Parameter(props) {

FILE: spug_web/src/pages/exec/template/Table.js
  class ComTable (line 14) | @observer
    method componentDidMount (line 16) | componentDidMount() {
    method render (line 34) | render() {

FILE: spug_web/src/pages/exec/template/store.js
  class Store (line 9) | class Store {
    method dataSource (line 24) | get dataSource() {

FILE: spug_web/src/pages/exec/transfer/Output.js
  function OutView (line 25) | function OutView(props) {

FILE: spug_web/src/pages/exec/transfer/index.js
  function TransferIndex (line 24) | function TransferIndex() {

FILE: spug_web/src/pages/exec/transfer/store.js
  class Store (line 8) | class Store {
    method items (line 12) | @computed get items() {
    method counter (line 25) | @computed get counter() {

FILE: spug_web/src/pages/home/Nav.js
  function NavIndex (line 14) | function NavIndex(props) {

FILE: spug_web/src/pages/home/NavForm.js
  function NavForm (line 13) | function NavForm(props) {

FILE: spug_web/src/pages/home/Notice.js
  function NoticeIndex (line 13) | function NoticeIndex(props) {

FILE: spug_web/src/pages/home/Todo.js
  function TodoIndex (line 9) | function TodoIndex(props) {

FILE: spug_web/src/pages/home/index.js
  function HomeIndex (line 13) | function HomeIndex() {

FILE: spug_web/src/pages/host/BatchSync.js
  function handleSubmit (line 20) | function handleSubmit() {
  function handleClose (line 30) | function handleClose() {

FILE: spug_web/src/pages/host/CloudImport.js
  function handleSubmit (line 25) | function handleSubmit() {
  function fetchRegions (line 45) | function fetchRegions() {

FILE: spug_web/src/pages/host/Detail.js
  function handleSubmit (line 45) | function handleSubmit() {
  function handleFetch (line 59) | function handleFetch() {
  function handleChange (line 70) | function handleChange(e, key) {
  function handleClose (line 78) | function handleClose() {
  function handleTagConfirm (line 83) | function handleTagConfirm(key) {
  function handleTagRemove (line 100) | function handleTagRemove(key, index) {

FILE: spug_web/src/pages/host/Form.js
  function handleSubmit (line 26) | function handleSubmit() {
  function handleConfirm (line 56) | function handleConfirm(formData) {
  function handleUploadChange (line 77) | function handleUploadChange(v) {
  function handleUpload (line 83) | function handleUpload(file, fileList) {

FILE: spug_web/src/pages/host/Group.js
  function handleSubmit (line 66) | function handleSubmit() {
  function handleRemoveHosts (line 79) | function handleRemoveHosts() {
  function handleRemove (line 89) | function handleRemove() {
  function handleAddRoot (line 99) | function handleAddRoot() {
  function handleAdd (line 108) | function handleAdd() {
  function _find_node (line 119) | function _find_node(list, key) {
  function handleDrag (line 128) | function handleDrag(v) {
  function handleRightClick (line 136) | function handleRightClick(v) {
  function handleExpand (line 143) | function handleExpand(keys, {_, node}) {
  function treeRender (line 149) | function treeRender(nodeData) {

FILE: spug_web/src/pages/host/IPAddress.js
  function IPAddress (line 8) | function IPAddress(props) {

FILE: spug_web/src/pages/host/Import.js
  function handleSubmit (line 22) | function handleSubmit() {
  function handleUpload (line 37) | function handleUpload(v) {
  function handleClose (line 45) | function handleClose() {

FILE: spug_web/src/pages/host/Selector.js
  function HostSelector (line 15) | function HostSelector(props) {

FILE: spug_web/src/pages/host/Table.js
  function ComTable (line 17) | function ComTable() {

FILE: spug_web/src/pages/host/index.js
  function openTerminal (line 26) | function openTerminal() {

FILE: spug_web/src/pages/host/store.js
  class Store (line 10) | class Store {
    method records (line 30) | @computed get records() {
    method dataSource (line 42) | @computed get dataSource() {
    method counter (line 52) | @computed get counter() {
    method treeData (line 69) | @computed get treeData() {

FILE: spug_web/src/pages/host/store2.js
  class Store (line 9) | class Store {
    method records (line 17) | @computed get records() {
    method dataSource (line 29) | @computed get dataSource() {
    method counter (line 38) | @computed get counter() {
    method treeData (line 57) | @computed get treeData() {

FILE: spug_web/src/pages/login/index.js
  function handleSubmit (line 44) | function handleSubmit() {
  function doLogin (line 73) | function doLogin(data) {
  function handleCaptcha (line 88) | function handleCaptcha() {

FILE: spug_web/src/pages/monitor/MonitorCard.js
  function CardItem (line 29) | function CardItem(props) {
  function MonitorCard (line 49) | function MonitorCard() {

FILE: spug_web/src/pages/monitor/Step1.js
  function handleTest (line 26) | function handleTest() {
  function handleChangeType (line 40) | function handleChangeType(v) {
  function handleAddGroup (line 46) | function handleAddGroup() {
  function canNext (line 66) | function canNext() {
  function toNext (line 76) | function toNext() {
  function getStyle (line 85) | function getStyle(t) {

FILE: spug_web/src/pages/monitor/Step2.js
  function handleSubmit (line 37) | function handleSubmit() {
  function canNext (line 52) | function canNext() {

FILE: spug_web/src/pages/monitor/Table.js
  class ComTable (line 14) | @observer
    method componentDidMount (line 16) | componentDidMount() {
    method render (line 48) | render() {

FILE: spug_web/src/pages/monitor/store.js
  class Store (line 11) | class Store {
    method dataSource (line 28) | @computed get dataSource() {
    method ovDataSource (line 37) | @computed get ovDataSource() {

FILE: spug_web/src/pages/schedule/Info.js
  class ComForm (line 13) | class ComForm extends React.Component {
    method constructor (line 14) | constructor(props) {
    method componentDidMount (line 22) | componentDidMount() {
    method render (line 28) | render() {

FILE: spug_web/src/pages/schedule/Record.js
  class Record (line 13) | @observer
    method constructor (line 15) | constructor(props) {
    method componentDidMount (line 23) | componentDidMount() {
    method render (line 42) | render() {

FILE: spug_web/src/pages/schedule/Step1.js
  function handleAddZone (line 24) | function handleAddZone() {
  function canNext (line 45) | function canNext() {
  function handleNext (line 50) | function handleNext() {
  function handleSelect (line 57) | function handleSelect(tpl) {

FILE: spug_web/src/pages/schedule/Step2.js
  function handleChange (line 16) | function handleChange(ids) {

FILE: spug_web/src/pages/schedule/Step3.js
  function handleSubmit (line 18) | function handleSubmit() {
  function handleArgs (line 35) | function handleArgs(key, val) {
  function handleCronArgs (line 39) | function handleCronArgs(key, val) {
  function _parse_args (line 46) | function _parse_args() {
  function _fetchNextRunTime (line 62) | function _fetchNextRunTime() {

FILE: spug_web/src/pages/schedule/Table.js
  class ComTable (line 14) | @observer
    method componentDidMount (line 16) | componentDidMount() {
    method render (line 124) | render() {

FILE: spug_web/src/pages/schedule/store.js
  class Store (line 10) | class Store {
    method dataSource (line 26) | @computed get dataSource() {

FILE: spug_web/src/pages/ssh/FileManager.js
  class FileManager (line 24) | class FileManager extends React.Component {
    method constructor (line 25) | constructor(props) {
    method componentDidMount (line 41) | componentDidMount() {
    method componentDidUpdate (line 45) | componentDidUpdate(prevProps) {
    method render (line 216) | render() {

FILE: spug_web/src/pages/ssh/Setting.js
  function Setting (line 12) | function Setting(props) {

FILE: spug_web/src/pages/ssh/Terminal.js
  function WebSSH (line 14) | function WebSSH(props) {

FILE: spug_web/src/pages/ssh/index.js
  function WebSSH (line 35) | function WebSSH(props) {

FILE: spug_web/src/pages/system/account/Form.js
  function handleSubmit (line 25) | function handleSubmit() {

FILE: spug_web/src/pages/system/account/Table.js
  class ComTable (line 15) | @observer
    method constructor (line 17) | constructor(props) {
    method componentDidMount (line 24) | componentDidMount() {
    method render (line 105) | render() {

FILE: spug_web/src/pages/system/account/store.js
  class Store (line 9) | class Store {
    method dataSource (line 18) | @computed get dataSource() {

FILE: spug_web/src/pages/system/login/Table.js
  class ComTable (line 12) | @observer
    method constructor (line 14) | constructor(props) {
    method componentDidMount (line 21) | componentDidMount() {
    method render (line 57) | render() {

FILE: spug_web/src/pages/system/login/store.js
  class Store (line 9) | class Store {
    method dataSource (line 17) | @computed get dataSource() {

FILE: spug_web/src/pages/system/role/DeployPerm.js
  class DeployPerm (line 15) | @observer
    method constructor (line 17) | constructor(props) {
    method componentDidMount (line 26) | componentDidMount() {
    method render (line 71) | render() {

FILE: spug_web/src/pages/system/role/Form.js
  function handleSubmit (line 16) | function handleSubmit() {

FILE: spug_web/src/pages/system/role/HostPerm.js
  function handleSubmit (line 23) | function handleSubmit() {
  function handleChange (line 33) | function handleChange(index, value) {

FILE: spug_web/src/pages/system/role/PagePerm.js
  class PagePerm (line 15) | @observer
    method constructor (line 17) | constructor(props) {
    method render (line 62) | render() {

FILE: spug_web/src/pages/system/role/Table.js
  class ComTable (line 17) | @observer
    method componentDidMount (line 19) | componentDidMount() {
    method render (line 68) | render() {

FILE: spug_web/src/pages/system/role/store.js
  class Store (line 11) | class Store {
    method dataSource (line 26) | @computed get dataSource() {
    method constructor (line 32) | constructor() {
    method idMap (line 36) | @computed get idMap() {

FILE: spug_web/src/pages/system/setting/About.js
  class About (line 14) | @observer
    method constructor (line 16) | constructor(props) {
    method componentDidMount (line 24) | componentDidMount() {
    method render (line 55) | render() {

FILE: spug_web/src/pages/system/setting/AlarmSetting.js
  function handleEmailTest (line 18) | function handleEmailTest() {
  function _doSubmit (line 27) | function _doSubmit(formData) {
  function handleSubmit (line 37) | function handleSubmit() {

FILE: spug_web/src/pages/system/setting/KeySetting.js
  function handleSubmit (line 14) | function handleSubmit() {
  function doModify (line 28) | function doModify() {

FILE: spug_web/src/pages/system/setting/LDAPSetting.js
  function handleSubmit (line 17) | function handleSubmit() {
  function ldapTest (line 28) | function ldapTest() {

FILE: spug_web/src/pages/system/setting/OpenService.js
  function handleSubmit (line 14) | function handleSubmit() {

FILE: spug_web/src/pages/system/setting/PushSetting.js
  function fetchBalance (line 27) | function fetchBalance() {
  function handleBind (line 37) | function handleBind() {
  function handleUnbind (line 49) | function handleUnbind() {

FILE: spug_web/src/pages/system/setting/SecuritySetting.js
  function handleChangeVerifyIP (line 28) | function handleChangeVerifyIP(v) {
  function handleChangeBindIP (line 37) | function handleChangeBindIP(v) {
  function handleChangeMFA (line 46) | function handleChangeMFA(v) {
  function handleCaptcha (line 51) | function handleCaptcha() {
  function handleMFAModify (line 58) | function handleMFAModify(v) {

FILE: spug_web/src/pages/system/setting/index.js
  class Index (line 20) | class Index extends React.Component {
    method constructor (line 21) | constructor(props) {
    method componentDidMount (line 28) | componentDidMount() {
    method render (line 32) | render() {

FILE: spug_web/src/pages/system/setting/store.js
  class Store (line 9) | class Store {

FILE: spug_web/src/pages/welcome/info/Basic.js
  function handleSubmit (line 29) | function handleSubmit() {

FILE: spug_web/src/pages/welcome/info/Reset.js
  function Reset (line 13) | function Reset(props) {

FILE: spug_web/src/pages/welcome/info/index.js
  function Index (line 13) | function Index() {

FILE: spug_web/src/pages/welcome/info/store.js
  class Store (line 9) | class Store {

FILE: spug_web/src/serviceWorker.js
  function register (line 28) | function register(config) {
  function registerValidSW (line 62) | function registerValidSW(swUrl, config) {
  function checkValidServiceWorker (line 106) | function checkValidServiceWorker(swUrl, config) {
  function unregister (line 134) | function unregister() {
Condensed preview — 362 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,038K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 46,
    "preview": "custom: ['https://www.spug.dev/sponsorship/']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "chars": 201,
    "preview": "---\nname: \"🐛 Bug Report\"\nabout: Report a reproducible bug or regression.\ntitle: 'Bug: '\n---\n\n<!--\n  Spug 版本信息可以在 系统管理/系统"
  },
  {
    "path": ".github/workflows/github_to_gitee.yml",
    "chars": 531,
    "preview": "name: github repos to gitee job\non:\n# 如果需要PR触发把push前的#去掉\n# push:\n  schedule:\n    # 每天北京时间1点跑\n    - cron:  '0 1 * * *'\njo"
  },
  {
    "path": ".gitignore",
    "chars": 8,
    "preview": "/.idea/\n"
  },
  {
    "path": "LICENSE",
    "chars": 34523,
    "preview": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C)"
  },
  {
    "path": "README.md",
    "chars": 2755,
    "preview": "<h1 align=\"center\">Spug</h1>\n\n<div align=\"center\">\n\nSpug是面向中小型企业设计的轻量级无Agent的自动化运维平台,整合了主机管理、主机批量执行、主机在线终端、应用发布部署、在线任务计划"
  },
  {
    "path": "docs/FQA.md",
    "chars": 120,
    "preview": "### install mysqlclient\n```shell\n# for centos 7\nyum install mariadb-devel python3-devel gcc\npip install mysqlclient\n```\n"
  },
  {
    "path": "docs/docker/Dockerfile",
    "chars": 1264,
    "preview": "FROM centos:7.9.2009\n\nENV TZ=Asia/Shanghai\nRUN yum install -y epel-release https://packages.endpointdev.com/rhel/7/os/x8"
  },
  {
    "path": "docs/docker/docker-compose.yml",
    "chars": 846,
    "preview": "version: \"3.3\"\nservices:\n  db:\n    image: mariadb:10.8\n    container_name: spug-db\n    restart: always\n    command: --ch"
  },
  {
    "path": "docs/docker/entrypoint.sh",
    "chars": 1086,
    "preview": "#!/bin/bash\n#\nset -e\n\nif [ -e /root/.bashrc ]; then\n    source /root/.bashrc\nfi\n\nif [ ! -d /data/spug/spug_api ]; then\n "
  },
  {
    "path": "docs/docker/init_spug",
    "chars": 145,
    "preview": "#!/bin/bash\n#\nset -e\nset -u\n\npython3 /data/spug/spug_api/manage.py updatedb\npython3 /data/spug/spug_api/manage.py user a"
  },
  {
    "path": "docs/docker/nginx.conf",
    "chars": 2385,
    "preview": "# For more information on configuration, see:\n#   * Official English Documentation: http://nginx.org/en/docs/\n#   * Offi"
  },
  {
    "path": "docs/docker/redis.conf",
    "chars": 46722,
    "preview": "# Redis configuration file example.\n#\n# Note that in order to read the configuration file, Redis must be\n# started with "
  },
  {
    "path": "docs/docker/spug.ini",
    "chars": 1042,
    "preview": "[supervisord]\nnodaemon=true\n\n[program:nginx]\ncommand = nginx -g \"daemon off;\"\nautostart = true\n\n[program:redis]\ncommand "
  },
  {
    "path": "docs/docker/ssh_config",
    "chars": 34,
    "preview": "Host *\n  StrictHostKeyChecking no\n"
  },
  {
    "path": "docs/install.sh",
    "chars": 5247,
    "preview": "#!/bin/bash\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.co"
  },
  {
    "path": "spug_api/.gitignore",
    "chars": 87,
    "preview": "*.pyc\n/venv/\n__pycache__/\n/.idea/\n/db.sqlite3\nmigrations/\n/access.log\n/repos/*\n/logs/*\n"
  },
  {
    "path": "spug_api/apps/account/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/history.py",
    "chars": 397,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/management/commands/set.py",
    "chars": 1177,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/management/commands/update.py",
    "chars": 3305,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/management/commands/updatedb.py",
    "chars": 694,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/management/commands/user.py",
    "chars": 2906,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/models.py",
    "chars": 5078,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/urls.py",
    "chars": 506,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/utils.py",
    "chars": 922,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/account/views.py",
    "chars": 11534,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/alarm/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/alarm/models.py",
    "chars": 2821,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/alarm/urls.py",
    "chars": 379,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/alarm/views.py",
    "chars": 4895,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/apis/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/apis/config.py",
    "chars": 2279,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/apis/deploy.py",
    "chars": 4566,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/apis/urls.py",
    "chars": 365,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/app/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/app/models.py",
    "chars": 4674,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/app/urls.py",
    "chars": 382,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/app/utils.py",
    "chars": 1275,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/app/views.py",
    "chars": 10138,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/config/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/config/models.py",
    "chars": 2788,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/config/urls.py",
    "chars": 503,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/config/utils.py",
    "chars": 1491,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/config/views.py",
    "chars": 13877,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/deploy/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/deploy/helper.py",
    "chars": 10982,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/deploy/models.py",
    "chars": 2704,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/deploy/urls.py",
    "chars": 568,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/deploy/utils.py",
    "chars": 14599,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/deploy/views.py",
    "chars": 16419,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/exec/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/exec/executors.py",
    "chars": 2604,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/exec/management/commands/runworker.py",
    "chars": 2746,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/exec/models.py",
    "chars": 2847,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/exec/transfer.py",
    "chars": 6672,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/exec/urls.py",
    "chars": 411,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/exec/views.py",
    "chars": 4998,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/file/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/file/urls.py",
    "chars": 297,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/file/utils.py",
    "chars": 2888,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/file/views.py",
    "chars": 3698,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/home/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/home/models.py",
    "chars": 1276,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/home/navigation.py",
    "chars": 2278,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/home/notice.py",
    "chars": 2490,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/home/urls.py",
    "chars": 526,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/home/views.py",
    "chars": 2972,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/host/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/host/add.py",
    "chars": 3302,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/host/extend.py",
    "chars": 2651,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/host/group.py",
    "chars": 4890,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/host/models.py",
    "chars": 3855,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/host/urls.py",
    "chars": 658,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/host/utils.py",
    "chars": 11565,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/host/views.py",
    "chars": 9486,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/monitor/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/monitor/executors.py",
    "chars": 4012,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/monitor/management/commands/runmonitor.py",
    "chars": 486,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/monitor/models.py",
    "chars": 1935,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/monitor/scheduler.py",
    "chars": 2852,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/monitor/urls.py",
    "chars": 325,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/monitor/utils.py",
    "chars": 1345,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/monitor/views.py",
    "chars": 6362,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/notify/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/notify/models.py",
    "chars": 2026,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/notify/urls.py",
    "chars": 256,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/notify/views.py",
    "chars": 699,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/repository/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/repository/models.py",
    "chars": 2052,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/repository/urls.py",
    "chars": 333,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/repository/utils.py",
    "chars": 4335,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/repository/views.py",
    "chars": 5671,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/builtin.py",
    "chars": 3341,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/executors.py",
    "chars": 2571,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/management/commands/runscheduler.py",
    "chars": 488,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/models.py",
    "chars": 2603,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/scheduler.py",
    "chars": 4358,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/urls.py",
    "chars": 340,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/utils.py",
    "chars": 3247,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/schedule/views.py",
    "chars": 7857,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/setting/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/setting/models.py",
    "chars": 1378,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/setting/urls.py",
    "chars": 641,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/setting/user.py",
    "chars": 977,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/setting/utils.py",
    "chars": 1472,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/apps/setting/views.py",
    "chars": 4937,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/consumer/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/consumer/consumers.py",
    "chars": 5079,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/consumer/routing.py",
    "chars": 472,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/consumer/utils.py",
    "chars": 1669,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/__init__.py",
    "chars": 286,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/channel.py",
    "chars": 606,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/decorators.py",
    "chars": 717,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/gitlib.py",
    "chars": 3806,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/helper.py",
    "chars": 2027,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/ldap.py",
    "chars": 1223,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/mail.py",
    "chars": 1123,
    "preview": "from email.header import Header\nfrom email.mime.text import MIMEText\nfrom email.utils import formataddr\nimport smtplib\n\n"
  },
  {
    "path": "spug_api/libs/middleware.py",
    "chars": 1689,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/mixins.py",
    "chars": 1207,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/parser.py",
    "chars": 4247,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/push.py",
    "chars": 1247,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/spug.py",
    "chars": 9805,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/ssh.py",
    "chars": 9041,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/utils.py",
    "chars": 3629,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/libs/validators.py",
    "chars": 739,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/logs/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spug_api/manage.py",
    "chars": 774,
    "preview": "#!/usr/bin/env python\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.de"
  },
  {
    "path": "spug_api/repos/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spug_api/repos/build/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spug_api/requirements.txt",
    "chars": 214,
    "preview": "apscheduler==3.7.0\nDjango==2.2.28\nasgiref==3.2.10\nchannels==2.3.1\nchannels_redis==2.4.1\nparamiko==2.11.0\ndjango-redis==4"
  },
  {
    "path": "spug_api/spug/__init__.py",
    "chars": 150,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/spug/asgi.py",
    "chars": 464,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/spug/routing.py",
    "chars": 301,
    "preview": "# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Release"
  },
  {
    "path": "spug_api/spug/settings.py",
    "chars": 3365,
    "preview": "\"\"\"\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Rel"
  },
  {
    "path": "spug_api/spug/urls.py",
    "chars": 1575,
    "preview": "\"\"\"spug URL Configuration\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spu"
  },
  {
    "path": "spug_api/spug/wsgi.py",
    "chars": 536,
    "preview": "\"\"\"\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.com>\n# Rel"
  },
  {
    "path": "spug_api/storage/transfer/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "spug_api/tools/migrate.py",
    "chars": 1380,
    "preview": "import django\nimport sys\nimport os\n\nBASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\nsys.path.appe"
  },
  {
    "path": "spug_api/tools/start-api.sh",
    "chars": 357,
    "preview": "#!/bin/bash\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.co"
  },
  {
    "path": "spug_api/tools/start-monitor.sh",
    "chars": 401,
    "preview": "#!/bin/bash\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.co"
  },
  {
    "path": "spug_api/tools/start-scheduler.sh",
    "chars": 404,
    "preview": "#!/bin/bash\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.co"
  },
  {
    "path": "spug_api/tools/start-worker.sh",
    "chars": 399,
    "preview": "#!/bin/bash\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.co"
  },
  {
    "path": "spug_api/tools/start-ws.sh",
    "chars": 327,
    "preview": "#!/bin/bash\n# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug\n# Copyright: (c) <spug.dev@gmail.co"
  },
  {
    "path": "spug_api/tools/supervisor-spug.ini",
    "chars": 854,
    "preview": "[program:spug-api]\ncommand = bash /data/spug/spug_api/tools/start-api.sh\nautostart = true\nstdout_logfile = /data/spug/sp"
  },
  {
    "path": "spug_web/.gitignore",
    "chars": 318,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "spug_web/README.md",
    "chars": 8,
    "preview": "spug web"
  },
  {
    "path": "spug_web/config-overrides.js",
    "chars": 440,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/jsconfig.json",
    "chars": 82,
    "preview": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"src\"\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "spug_web/package.json",
    "chars": 1271,
    "preview": "{\n  \"name\": \"spug_web\",\n  \"version\": \"3.0.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@ant-design/icons\": \"^4.3.0\",\n"
  },
  {
    "path": "spug_web/public/index.html",
    "chars": 1700,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.i"
  },
  {
    "path": "spug_web/public/manifest.json",
    "chars": 462,
    "preview": "{\n  \"short_name\": \"Spug\",\n  \"name\": \"Spug\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 2"
  },
  {
    "path": "spug_web/public/robots.txt",
    "chars": 25,
    "preview": "User-agent: *\nDisallow: /"
  },
  {
    "path": "spug_web/src/App.js",
    "chars": 614,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/ACEditor.js",
    "chars": 804,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/Action.js",
    "chars": 1096,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/AppSelector.js",
    "chars": 3315,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/AuthButton.js",
    "chars": 483,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/AuthCard.js",
    "chars": 512,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/AuthDiv.js",
    "chars": 482,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/AuthFragment.js",
    "chars": 368,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/Breadcrumb.js",
    "chars": 961,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/Link.js",
    "chars": 391,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/LinkButton.js",
    "chars": 526,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/NotFound.js",
    "chars": 390,
    "preview": "import React from 'react';\nimport styles from './index.module.less';\n\nexport default function NotFound() {\n  return (\n  "
  },
  {
    "path": "spug_web/src/components/SearchForm.js",
    "chars": 809,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/StatisticsCard.js",
    "chars": 1040,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/TableCard.js",
    "chars": 6108,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/index.js",
    "chars": 874,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/components/index.module.less",
    "chars": 4078,
    "preview": ".searchForm {\n  padding: 24px 24px 0 24px;\n  background-color: #fff;\n  border-radius: 2px;\n  margin-bottom: 16px;\n\n  :gl"
  },
  {
    "path": "spug_web/src/gStore.js",
    "chars": 1174,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/index.js",
    "chars": 1068,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/index.less",
    "chars": 611,
    "preview": "@import '~antd/dist/antd.less';\n\nbody {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, P"
  },
  {
    "path": "spug_web/src/layout/Footer.js",
    "chars": 1125,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/layout/Header.js",
    "chars": 2784,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/layout/Notification.js",
    "chars": 4425,
    "preview": "import React, { useState, useEffect } from 'react';\nimport { Menu, List, Dropdown, Badge, Button, notification } from 'a"
  },
  {
    "path": "spug_web/src/layout/Sider.js",
    "chars": 2099,
    "preview": "import React, { useState, useEffect } from 'react';\nimport { Layout, Menu } from 'antd';\nimport { hasPermission, history"
  },
  {
    "path": "spug_web/src/layout/index.js",
    "chars": 1660,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/layout/layout.module.less",
    "chars": 2231,
    "preview": ".header {\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  padding: 0 12px 0 0;;\n  height: 48"
  },
  {
    "path": "spug_web/src/libs/functools.js",
    "chars": 2861,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/libs/history.js",
    "chars": 244,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/libs/http.js",
    "chars": 1707,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/libs/index.js",
    "chars": 370,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/libs/libs.module.css",
    "chars": 569,
    "preview": ".container {\n    display: flex;\n    height: 80%;\n    align-items: center;\n}\n\n.imgBlock {\n    flex: 0 0 62.5%;\n    width:"
  },
  {
    "path": "spug_web/src/libs/router.js",
    "chars": 1478,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/alarm/Table.js",
    "chars": 2617,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/alarm/index.js",
    "chars": 1133,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/alarm/store.js",
    "chars": 850,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/contact/Form.js",
    "chars": 5746,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/contact/Table.js",
    "chars": 2436,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/contact/index.js",
    "chars": 988,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/contact/store.js",
    "chars": 896,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/group/Form.js",
    "chars": 2089,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/group/Table.js",
    "chars": 2661,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/group/index.js",
    "chars": 987,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/alarm/group/store.js",
    "chars": 1005,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/config/app/Form.js",
    "chars": 1622,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/config/app/Rel.js",
    "chars": 2813,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  },
  {
    "path": "spug_web/src/pages/config/app/Table.js",
    "chars": 2662,
    "preview": "/**\n * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug\n * Copyright (c) <spug.dev@gmail.com>\n * Re"
  }
]

// ... and 162 more files (download for full content)

About this extraction

This page contains the full source code of the openspug/spug GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 362 files (958.3 KB), approximately 257.2k tokens, and a symbol index with 936 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!